@parel/sandbox-vercel 0.2.9 → 0.3.1
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/index.js +283 -50
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -31,8 +31,8 @@ function createSandboxCapabilityViews(capability, store) {
|
|
|
31
31
|
return proc.exec(["sh", "-lc", command], opts);
|
|
32
32
|
};
|
|
33
33
|
const filesystem = {
|
|
34
|
-
async readFile(path) {
|
|
35
|
-
return requireFs().readFile(path);
|
|
34
|
+
async readFile(path, opts) {
|
|
35
|
+
return requireFs().readFile(path, opts);
|
|
36
36
|
},
|
|
37
37
|
async writeFile(path, content) {
|
|
38
38
|
await requireFs().writeFile(path, content);
|
|
@@ -237,6 +237,8 @@ var parel_plugin_default = {
|
|
|
237
237
|
|
|
238
238
|
// src/index.ts
|
|
239
239
|
var STORE_KEY = "vercel_sandbox_name";
|
|
240
|
+
var PROCESS_VIEW_STORE_PREFIX2 = "sandbox_process:";
|
|
241
|
+
var PORT_VIEW_STORE_PREFIX2 = "sandbox_port:";
|
|
240
242
|
function stringConfig(value) {
|
|
241
243
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
242
244
|
}
|
|
@@ -317,7 +319,26 @@ var index_default = definePlugin({
|
|
|
317
319
|
const teamId = stringConfig(ctx.config.teamId);
|
|
318
320
|
const projectId = stringConfig(ctx.config.projectId);
|
|
319
321
|
const destroyOnSessionEnd = ctx.config.destroyOnSessionEnd !== false;
|
|
322
|
+
const externalName = stringConfig(ctx.config.name);
|
|
323
|
+
const istore = ctx.instanceStore;
|
|
324
|
+
const state = istore ? {
|
|
325
|
+
async get(key) {
|
|
326
|
+
return (await istore.get(key))?.value ?? null;
|
|
327
|
+
},
|
|
328
|
+
set(key, value) {
|
|
329
|
+
return istore.set(key, value);
|
|
330
|
+
},
|
|
331
|
+
delete(key) {
|
|
332
|
+
return istore.delete(key);
|
|
333
|
+
},
|
|
334
|
+
list(prefix) {
|
|
335
|
+
return istore.list(prefix);
|
|
336
|
+
}
|
|
337
|
+
} : ctx.store;
|
|
338
|
+
const managed = Boolean(istore) && !externalName;
|
|
320
339
|
let sandbox = null;
|
|
340
|
+
let sandboxRecovery = null;
|
|
341
|
+
let sandboxEpoch = 0;
|
|
321
342
|
function credentials() {
|
|
322
343
|
if (!token || !teamId || !projectId) {
|
|
323
344
|
ctx.log.warn("Vercel Sandbox token, teamId, and projectId are required");
|
|
@@ -325,50 +346,238 @@ var index_default = definePlugin({
|
|
|
325
346
|
}
|
|
326
347
|
return { token, teamId, projectId };
|
|
327
348
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
return
|
|
349
|
+
async function createRawSandbox() {
|
|
350
|
+
const creds = credentials();
|
|
351
|
+
if (!creds) return null;
|
|
352
|
+
const params = { ...buildCreateParams(ctx.config), ...creds };
|
|
353
|
+
const created = await VercelSandboxClient.create(params);
|
|
354
|
+
ctx.log.info(`Vercel sandbox created: ${created.name}`);
|
|
355
|
+
return created;
|
|
331
356
|
}
|
|
332
|
-
async function
|
|
357
|
+
async function reconnectSandbox(name) {
|
|
358
|
+
const creds = credentials();
|
|
359
|
+
if (!creds) return null;
|
|
360
|
+
try {
|
|
361
|
+
const s = await VercelSandboxClient.get({ name, ...creds });
|
|
362
|
+
ctx.log.info(`Vercel sandbox connected: ${name}`);
|
|
363
|
+
return s;
|
|
364
|
+
} catch (err) {
|
|
365
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
366
|
+
ctx.log.warn(`Failed to connect Vercel sandbox ${name}: ${message}`);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function killSandboxById(name) {
|
|
371
|
+
const creds = credentials();
|
|
372
|
+
if (!creds) return false;
|
|
373
|
+
try {
|
|
374
|
+
const s = await VercelSandboxClient.get({ name, ...creds });
|
|
375
|
+
await s.delete();
|
|
376
|
+
return true;
|
|
377
|
+
} catch (err) {
|
|
378
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
379
|
+
ctx.log.warn(`Failed to kill Vercel sandbox ${name}: ${message}`);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function moveLegacyRecords(promote) {
|
|
384
|
+
for (const prefix of [PROCESS_VIEW_STORE_PREFIX2, PORT_VIEW_STORE_PREFIX2]) {
|
|
385
|
+
for (const key of await ctx.store.list(prefix)) {
|
|
386
|
+
if (promote) {
|
|
387
|
+
const record = await ctx.store.get(key);
|
|
388
|
+
if (record !== null) await state.set(key, record);
|
|
389
|
+
}
|
|
390
|
+
await ctx.store.delete(key);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function clearViewRecords() {
|
|
395
|
+
for (const prefix of [PROCESS_VIEW_STORE_PREFIX2, PORT_VIEW_STORE_PREFIX2]) {
|
|
396
|
+
for (const key of await state.list(prefix)) {
|
|
397
|
+
await state.delete(key);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function migrateLegacyHandle() {
|
|
402
|
+
if (!istore) return;
|
|
403
|
+
const legacy = await ctx.store.get(STORE_KEY);
|
|
404
|
+
if (!legacy) return;
|
|
405
|
+
const authoritative = await istore.get(STORE_KEY);
|
|
406
|
+
if (!authoritative) {
|
|
407
|
+
const reconnected = await reconnectSandbox(legacy);
|
|
408
|
+
if (!reconnected) {
|
|
409
|
+
await ctx.store.delete(STORE_KEY);
|
|
410
|
+
await moveLegacyRecords(false);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (await istore.cas(STORE_KEY, null, legacy)) {
|
|
414
|
+
await ctx.store.delete(STORE_KEY);
|
|
415
|
+
await moveLegacyRecords(true);
|
|
416
|
+
ctx.log.info(`Vercel sandbox ${legacy} migrated to the instance store`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if ((await istore.get(STORE_KEY))?.value !== legacy) {
|
|
421
|
+
await killSandboxById(legacy);
|
|
422
|
+
ctx.log.info(`Vercel legacy sandbox ${legacy} reaped (instance already has a sandbox)`);
|
|
423
|
+
}
|
|
424
|
+
await ctx.store.delete(STORE_KEY);
|
|
425
|
+
await moveLegacyRecords(false);
|
|
426
|
+
}
|
|
427
|
+
async function acquireSharedSandbox() {
|
|
428
|
+
if (!istore) throw new Error("acquireSharedSandbox requires an instance store");
|
|
429
|
+
await migrateLegacyHandle();
|
|
430
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
431
|
+
const entry = await istore.get(STORE_KEY);
|
|
432
|
+
if (entry) {
|
|
433
|
+
const reconnected = await reconnectSandbox(entry.value);
|
|
434
|
+
if (reconnected) {
|
|
435
|
+
const current = await istore.get(STORE_KEY);
|
|
436
|
+
if (current?.value === entry.value) return reconnected;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const fresh2 = await createRawSandbox();
|
|
440
|
+
if (!fresh2) return null;
|
|
441
|
+
if (await istore.cas(STORE_KEY, entry.version, fresh2.name)) {
|
|
442
|
+
ctx.log.warn(
|
|
443
|
+
`Vercel filesystem reset: sandbox ${entry.value} was unreachable, swapped in ${fresh2.name} (files in the previous sandbox are lost)`
|
|
444
|
+
);
|
|
445
|
+
await killSandboxById(entry.value);
|
|
446
|
+
await clearViewRecords();
|
|
447
|
+
return fresh2;
|
|
448
|
+
}
|
|
449
|
+
await killSandboxById(fresh2.name);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const fresh = await createRawSandbox();
|
|
453
|
+
if (!fresh) return null;
|
|
454
|
+
if (await istore.cas(STORE_KEY, null, fresh.name)) return fresh;
|
|
455
|
+
await killSandboxById(fresh.name);
|
|
456
|
+
}
|
|
457
|
+
throw new Error("Vercel sandbox acquisition kept losing instance-store races; giving up");
|
|
458
|
+
}
|
|
459
|
+
async function acquirePerSession() {
|
|
333
460
|
const creds = credentials();
|
|
334
461
|
if (!creds) return null;
|
|
335
462
|
const savedName = await ctx.store.get(STORE_KEY);
|
|
336
463
|
const name = stringConfig(ctx.config.name) ?? savedName;
|
|
337
464
|
const params = { ...buildCreateParams(ctx.config), ...creds };
|
|
465
|
+
let next;
|
|
338
466
|
if (name) {
|
|
339
|
-
|
|
467
|
+
next = await VercelSandboxClient.getOrCreate({
|
|
340
468
|
...params,
|
|
341
469
|
name
|
|
342
470
|
});
|
|
343
471
|
} else {
|
|
344
|
-
|
|
472
|
+
next = await VercelSandboxClient.create(params);
|
|
345
473
|
}
|
|
346
|
-
await ctx.store.set(STORE_KEY,
|
|
347
|
-
ctx.log.info(`Vercel sandbox ready: ${
|
|
348
|
-
return
|
|
474
|
+
await ctx.store.set(STORE_KEY, next.name);
|
|
475
|
+
ctx.log.info(`Vercel sandbox ready: ${next.name}`);
|
|
476
|
+
return next;
|
|
349
477
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
478
|
+
function acquire() {
|
|
479
|
+
return managed ? acquireSharedSandbox() : acquirePerSession();
|
|
480
|
+
}
|
|
481
|
+
async function ensureSandbox() {
|
|
482
|
+
const cached = sandbox;
|
|
483
|
+
if (cached) {
|
|
484
|
+
if (!managed) return cached;
|
|
485
|
+
const authoritative = await istore?.get(STORE_KEY);
|
|
486
|
+
if (authoritative?.value === cached.name) return cached;
|
|
487
|
+
if (sandbox === cached) sandbox = null;
|
|
488
|
+
}
|
|
489
|
+
if (!sandboxRecovery) {
|
|
490
|
+
const epoch = sandboxEpoch;
|
|
491
|
+
sandboxRecovery = acquire().then(async (s) => {
|
|
492
|
+
if (epoch !== sandboxEpoch) {
|
|
493
|
+
if (managed) {
|
|
494
|
+
throw new Error("Vercel sandbox recovery discarded: session torn down");
|
|
495
|
+
}
|
|
496
|
+
if (s && destroyOnSessionEnd) {
|
|
497
|
+
await killSandboxById(s.name);
|
|
498
|
+
await ctx.store.delete(STORE_KEY);
|
|
499
|
+
}
|
|
500
|
+
throw new Error("Vercel sandbox was torn down during recovery");
|
|
501
|
+
}
|
|
502
|
+
sandbox = s;
|
|
503
|
+
return s;
|
|
504
|
+
}).finally(() => {
|
|
505
|
+
sandboxRecovery = null;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return sandboxRecovery;
|
|
509
|
+
}
|
|
510
|
+
async function requireSandbox() {
|
|
511
|
+
const s = await ensureSandbox();
|
|
512
|
+
if (!s) throw new Error("Vercel sandbox not available");
|
|
513
|
+
return s;
|
|
514
|
+
}
|
|
515
|
+
async function releaseSandbox() {
|
|
516
|
+
sandboxEpoch++;
|
|
517
|
+
if (sandboxRecovery) {
|
|
518
|
+
await sandboxRecovery.catch(() => {
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
sandbox = null;
|
|
522
|
+
}
|
|
523
|
+
async function destroySandbox() {
|
|
524
|
+
sandboxEpoch++;
|
|
525
|
+
if (sandboxRecovery) {
|
|
526
|
+
await sandboxRecovery.catch(() => {
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (istore) {
|
|
360
530
|
sandbox = null;
|
|
531
|
+
const entry = await istore.get(STORE_KEY);
|
|
532
|
+
if (entry) {
|
|
533
|
+
const retired = await istore.casDelete?.(STORE_KEY, entry.version) ?? false;
|
|
534
|
+
if (!retired && istore.casDelete) {
|
|
535
|
+
ctx.log.warn("Vercel stop skipped: the instance sandbox changed mid-stop");
|
|
536
|
+
await ctx.store.delete(STORE_KEY);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!istore.casDelete) await istore.delete(STORE_KEY);
|
|
540
|
+
if (await killSandboxById(entry.value)) {
|
|
541
|
+
ctx.log.info(`Vercel sandbox destroyed: ${entry.value}`);
|
|
542
|
+
}
|
|
543
|
+
await clearViewRecords();
|
|
544
|
+
}
|
|
545
|
+
await ctx.store.delete(STORE_KEY);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const target = sandbox?.name ?? await ctx.store.get(STORE_KEY);
|
|
549
|
+
sandbox = null;
|
|
550
|
+
if (target && await killSandboxById(target)) {
|
|
551
|
+
ctx.log.info(`Vercel sandbox destroyed: ${target}`);
|
|
552
|
+
}
|
|
553
|
+
await clearViewRecords();
|
|
554
|
+
await ctx.store.delete(STORE_KEY);
|
|
555
|
+
}
|
|
556
|
+
async function disposeSandbox() {
|
|
557
|
+
sandboxEpoch++;
|
|
558
|
+
if (sandboxRecovery) {
|
|
559
|
+
await sandboxRecovery.catch(() => {
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const current = sandbox;
|
|
563
|
+
sandbox = null;
|
|
564
|
+
if (!current) return;
|
|
565
|
+
if (destroyOnSessionEnd) {
|
|
566
|
+
await current.delete();
|
|
567
|
+
await ctx.store.delete(STORE_KEY);
|
|
568
|
+
} else {
|
|
569
|
+
await current.stop();
|
|
361
570
|
}
|
|
362
571
|
}
|
|
363
572
|
async function execCommand(command, opts) {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
);
|
|
573
|
+
const s = await requireSandbox();
|
|
574
|
+
const finished = await s.runCommand(runParams(command, opts));
|
|
367
575
|
return commandResult(finished, opts);
|
|
368
576
|
}
|
|
369
577
|
async function shellCommand(command, opts) {
|
|
370
578
|
const shell = opts?.shell ?? "sh";
|
|
371
|
-
const
|
|
579
|
+
const s = await requireSandbox();
|
|
580
|
+
const finished = await s.runCommand({
|
|
372
581
|
cmd: shell,
|
|
373
582
|
args: ["-lc", command],
|
|
374
583
|
cwd: opts?.cwd,
|
|
@@ -415,30 +624,32 @@ var index_default = definePlugin({
|
|
|
415
624
|
},
|
|
416
625
|
fs: {
|
|
417
626
|
async readFile(path, opts) {
|
|
418
|
-
const
|
|
627
|
+
const s = await requireSandbox();
|
|
628
|
+
const content = await s.fs.readFile(path, {
|
|
419
629
|
encoding: opts?.encoding === "base64" ? null : "utf8"
|
|
420
630
|
});
|
|
421
631
|
const text = Buffer.isBuffer(content) ? content.toString("base64") : content;
|
|
422
632
|
return opts?.maxChars ? limitOutput(text, opts.maxChars) : text;
|
|
423
633
|
},
|
|
424
634
|
async writeFile(path, content, opts) {
|
|
635
|
+
const s = await requireSandbox();
|
|
425
636
|
const data = opts?.encoding === "base64" ? Buffer.from(content, "base64") : content;
|
|
426
|
-
if (opts?.append &&
|
|
427
|
-
await
|
|
637
|
+
if (opts?.append && s.fs.appendFile) {
|
|
638
|
+
await s.fs.appendFile(path, data);
|
|
428
639
|
} else {
|
|
429
|
-
await
|
|
640
|
+
await s.fs.writeFile(path, data);
|
|
430
641
|
}
|
|
431
642
|
},
|
|
432
643
|
async listDir(path) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
);
|
|
644
|
+
const s = await requireSandbox();
|
|
645
|
+
return (await s.fs.readdir(path, { withFileTypes: true })).map((entry) => ({
|
|
646
|
+
name: entry.name,
|
|
647
|
+
type: mapDirentType(entry)
|
|
648
|
+
}));
|
|
439
649
|
},
|
|
440
650
|
async stat(path) {
|
|
441
|
-
const
|
|
651
|
+
const s = await requireSandbox();
|
|
652
|
+
const stats = await s.fs.stat(path);
|
|
442
653
|
return {
|
|
443
654
|
path,
|
|
444
655
|
type: mapStatsType(stats),
|
|
@@ -448,23 +659,28 @@ var index_default = definePlugin({
|
|
|
448
659
|
};
|
|
449
660
|
},
|
|
450
661
|
async exists(path) {
|
|
451
|
-
|
|
662
|
+
const s = await requireSandbox();
|
|
663
|
+
return s.fs.exists(path);
|
|
452
664
|
},
|
|
453
665
|
async mkdir(path, opts) {
|
|
454
|
-
await requireSandbox()
|
|
666
|
+
const s = await requireSandbox();
|
|
667
|
+
await s.fs.mkdir(path, { recursive: opts?.recursive });
|
|
455
668
|
},
|
|
456
669
|
async remove(path, opts) {
|
|
457
|
-
await requireSandbox()
|
|
670
|
+
const s = await requireSandbox();
|
|
671
|
+
await s.fs.rm(path, { recursive: opts?.recursive, force: true });
|
|
458
672
|
},
|
|
459
673
|
async rename(from, to) {
|
|
460
|
-
await requireSandbox()
|
|
674
|
+
const s = await requireSandbox();
|
|
675
|
+
await s.fs.rename(from, to);
|
|
461
676
|
}
|
|
462
677
|
},
|
|
463
678
|
process: {
|
|
464
679
|
exec: execCommand,
|
|
465
680
|
shell: shellCommand,
|
|
466
681
|
async spawn(command, opts) {
|
|
467
|
-
const
|
|
682
|
+
const s = await requireSandbox();
|
|
683
|
+
const handle = await s.runCommand({
|
|
468
684
|
...runParams(command, opts),
|
|
469
685
|
detached: true
|
|
470
686
|
});
|
|
@@ -473,35 +689,52 @@ var index_default = definePlugin({
|
|
|
473
689
|
},
|
|
474
690
|
ports: {
|
|
475
691
|
async expose(port) {
|
|
476
|
-
|
|
692
|
+
const s = await requireSandbox();
|
|
693
|
+
return { port, url: s.domain(port), protocol: "https" };
|
|
477
694
|
}
|
|
478
695
|
},
|
|
479
696
|
lifecycle: {
|
|
480
697
|
async isRunning() {
|
|
481
|
-
|
|
698
|
+
if (!sandbox) return false;
|
|
699
|
+
return String(sandbox.status ?? "running") === "running";
|
|
482
700
|
},
|
|
483
701
|
async stop() {
|
|
484
|
-
|
|
702
|
+
if (istore) {
|
|
703
|
+
await destroySandbox();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (!sandbox) throw new Error("Vercel sandbox not available");
|
|
707
|
+
await sandbox.stop();
|
|
485
708
|
},
|
|
486
709
|
async extendTimeout(timeoutMs) {
|
|
487
|
-
await requireSandbox()
|
|
710
|
+
const s = await requireSandbox();
|
|
711
|
+
await s.extendTimeout?.(timeoutMs);
|
|
488
712
|
}
|
|
489
713
|
}
|
|
490
714
|
};
|
|
491
715
|
ctx.hook(LifecycleEvent.SessionStart, async () => {
|
|
492
|
-
await
|
|
716
|
+
sandbox = await acquire();
|
|
493
717
|
});
|
|
494
718
|
ctx.hook(LifecycleEvent.SessionResume, async () => {
|
|
495
|
-
|
|
719
|
+
sandbox = null;
|
|
720
|
+
sandbox = await acquire();
|
|
496
721
|
});
|
|
497
722
|
ctx.hook(LifecycleEvent.SessionSuspend, async () => {
|
|
498
|
-
if (sandbox) await ctx.store.set(STORE_KEY, sandbox.name);
|
|
723
|
+
if (!istore && sandbox) await ctx.store.set(STORE_KEY, sandbox.name);
|
|
499
724
|
});
|
|
500
725
|
ctx.hook(LifecycleEvent.SessionEnd, async () => {
|
|
501
|
-
|
|
726
|
+
if (!istore) {
|
|
727
|
+
await disposeSandbox();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (ctx.instance?.ephemeral) {
|
|
731
|
+
await destroySandbox();
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
await releaseSandbox();
|
|
502
735
|
});
|
|
503
736
|
ctx.provide(PAREL_SANDBOX_CAPABILITY, capability);
|
|
504
|
-
const views = createSandboxCapabilityViews(capability,
|
|
737
|
+
const views = createSandboxCapabilityViews(capability, state);
|
|
505
738
|
ctx.provide("filesystem", views.filesystem);
|
|
506
739
|
ctx.provide("exec", views.exec);
|
|
507
740
|
ctx.provide("process", views.process);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parel/sandbox-vercel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "PAREL sandbox capability provider plugin for Vercel Sandbox.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@vercel/sandbox": "^2.1.1",
|
|
30
|
-
"@parel/capability-sandbox": "0.
|
|
31
|
-
"@parel/plugin-sdk": "0.
|
|
30
|
+
"@parel/capability-sandbox": "0.3.0",
|
|
31
|
+
"@parel/plugin-sdk": "0.11.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^25.9.1",
|