withruntime 0.12.0 → 0.16.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/BLAXEL.md +97 -32
- package/COMPATIBILITY.md +120 -5
- package/DAYTONA.md +34 -3
- package/E2B.md +34 -5
- package/README.md +36 -3
- package/README.src.md +35 -2
- package/VERCEL.md +52 -5
- package/compatibility-lock.json +71 -111
- package/dist/billing-cli.js +8 -4
- package/dist/blaxel/client.d.ts +11 -0
- package/dist/blaxel/client.js +28 -0
- package/dist/blaxel/errors.d.ts +65 -0
- package/dist/blaxel/errors.js +114 -0
- package/dist/blaxel/index.d.ts +10 -3
- package/dist/blaxel/index.js +23 -4
- package/dist/blaxel/preview.d.ts +29 -4
- package/dist/blaxel/preview.js +79 -5
- package/dist/blaxel/sandbox.d.ts +1 -1
- package/dist/blaxel/sandbox.js +2 -1
- package/dist/blaxel/session.js +4 -2
- package/dist/cli-extras.js +2 -1
- package/dist/cli-words.js +1 -0
- package/dist/cli.d.ts +17 -2
- package/dist/cli.js +291 -85
- package/dist/client.d.ts +1 -1
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +15 -0
- package/dist/daytona/client.d.ts +4 -0
- package/dist/daytona/client.js +8 -3
- package/dist/daytona/daytona.d.ts +4 -1
- package/dist/daytona/daytona.js +3 -2
- package/dist/e2b/sandbox.d.ts +7 -6
- package/dist/e2b/sandbox.js +9 -12
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/freestyle/index.d.ts +77 -5
- package/dist/freestyle/index.js +380 -48
- package/dist/identity.js +36 -18
- package/dist/index.d.ts +4 -3
- package/dist/machine.d.ts +39 -13
- package/dist/machine.js +72 -25
- package/dist/mcp.js +157 -20
- package/dist/modal/index.d.ts +15 -0
- package/dist/modal/index.js +35 -7
- package/dist/morph/index.d.ts +255 -0
- package/dist/morph/index.js +641 -0
- package/dist/network-cli.js +2 -2
- package/dist/product-names.d.ts +2 -2
- package/dist/product-names.js +2 -2
- package/dist/products/api-keys.d.ts +73 -0
- package/dist/products/api-keys.js +23 -0
- package/dist/products/apps.d.ts +12 -31
- package/dist/products/apps.js +8 -113
- package/dist/products/balancers.d.ts +87 -0
- package/dist/products/balancers.js +8 -0
- package/dist/products/builds.d.ts +30 -0
- package/dist/products/builds.js +6 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/jobs.d.ts +2 -2
- package/dist/products/jobs.js +1 -1
- package/dist/products/limits.d.ts +16 -2
- package/dist/products/limits.js +2 -2
- package/dist/products/mcp.d.ts +5 -0
- package/dist/products/network-products.d.ts +21 -3
- package/dist/products/network-products.js +3 -1
- package/dist/products/observability.d.ts +7 -2
- package/dist/products/previews.d.ts +6 -3
- package/dist/products/previews.js +1 -1
- package/dist/products/resources.d.ts +54 -0
- package/dist/products/resources.js +122 -0
- package/dist/products/secrets.d.ts +22 -0
- package/dist/products/secrets.js +8 -0
- package/dist/products/volumes.d.ts +3 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +27 -2
- package/dist/snapshots.d.ts +4 -0
- package/dist/ssh.d.ts +35 -0
- package/dist/ssh.js +93 -1
- package/dist/transport.d.ts +14 -8
- package/dist/transport.js +70 -15
- package/dist/types.d.ts +33 -5
- package/dist/vercel/command.d.ts +48 -1
- package/dist/vercel/command.js +237 -6
- package/dist/vercel/sandbox-user.js +2 -1
- package/dist/vercel/sandbox.d.ts +17 -3
- package/dist/vercel/sandbox.js +46 -3
- package/dist/wallet.d.ts +4 -1
- package/dist/wallet.js +2 -1
- package/package.json +7 -14
- package/dist/cli/logs.test.d.ts +0 -1
- package/dist/cli/logs.test.js +0 -272
- package/dist/products/logs.test.d.ts +0 -1
- package/dist/products/logs.test.js +0 -343
package/dist/freestyle/index.js
CHANGED
|
@@ -31,23 +31,27 @@ export class FreestyleTransportError extends TypeError {
|
|
|
31
31
|
return { name: this.name, message: this.message, errors: this.errors };
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/** A Runtime error as Freestyle's client throws it. */
|
|
35
|
+
function translate(error) {
|
|
36
|
+
if (error instanceof ConnectionError)
|
|
37
|
+
return new FreestyleTransportError(error, [
|
|
38
|
+
{ name: error.name, message: error.message, code: error.code },
|
|
39
|
+
]);
|
|
40
|
+
if (!(error instanceof RuntimeError))
|
|
41
|
+
return error;
|
|
42
|
+
const translated = new FreestyleApiError(error.status, {
|
|
43
|
+
code: error.code.toUpperCase(),
|
|
44
|
+
message: error.message,
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(translated, "cause", { value: error });
|
|
47
|
+
return translated;
|
|
48
|
+
}
|
|
34
49
|
async function api(call) {
|
|
35
50
|
try {
|
|
36
51
|
return await call();
|
|
37
52
|
}
|
|
38
53
|
catch (error) {
|
|
39
|
-
|
|
40
|
-
throw new FreestyleTransportError(error, [
|
|
41
|
-
{ name: error.name, message: error.message, code: error.code },
|
|
42
|
-
]);
|
|
43
|
-
if (!(error instanceof RuntimeError))
|
|
44
|
-
throw error;
|
|
45
|
-
const translated = new FreestyleApiError(error.status, {
|
|
46
|
-
code: error.code.toUpperCase(),
|
|
47
|
-
message: error.message,
|
|
48
|
-
});
|
|
49
|
-
Object.defineProperty(translated, "cause", { value: error });
|
|
50
|
-
throw translated;
|
|
54
|
+
throw translate(error);
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
const notFound = (what) => new FreestyleApiError(404, { code: "NOT_FOUND", message: `${what} not found` });
|
|
@@ -555,63 +559,295 @@ const dimension = (value, fallback, name) => {
|
|
|
555
559
|
throw new TypeError(`${name} must be a whole number from 1 to 1000`);
|
|
556
560
|
return value;
|
|
557
561
|
};
|
|
558
|
-
const
|
|
559
|
-
"
|
|
560
|
-
"
|
|
561
|
-
"
|
|
562
|
-
"
|
|
563
|
-
"
|
|
562
|
+
const PTY_EVENTS = [
|
|
563
|
+
"reconnect",
|
|
564
|
+
"connectTimeoutMs",
|
|
565
|
+
"onStateChange",
|
|
566
|
+
"onReplayStart",
|
|
567
|
+
"onReplayEnd",
|
|
564
568
|
"onData",
|
|
565
569
|
"onExit",
|
|
566
570
|
"onClose",
|
|
567
571
|
"onError",
|
|
568
572
|
];
|
|
573
|
+
const PTY_OPEN = ["exec", "cols", "rows", "linuxUser", "slug", ...PTY_EVENTS];
|
|
574
|
+
/** Freestyle 0.2.18 checks its timers before anything is opened. */
|
|
575
|
+
function connectionOptions(options) {
|
|
576
|
+
const { reconnect } = options;
|
|
577
|
+
if (reconnect !== undefined && reconnect !== null && typeof reconnect !== "boolean") {
|
|
578
|
+
if (typeof reconnect !== "object")
|
|
579
|
+
throw new TypeError("reconnect must be true, false or reconnect options");
|
|
580
|
+
only("Freestyle pty reconnect", reconnect, ["timeoutMs", "initialDelayMs", "maxDelayMs"]);
|
|
581
|
+
}
|
|
582
|
+
const timers = {
|
|
583
|
+
connectTimeoutMs: options.connectTimeoutMs,
|
|
584
|
+
...(typeof reconnect === "object" && reconnect ? reconnect : {}),
|
|
585
|
+
};
|
|
586
|
+
for (const [name, value] of Object.entries(timers))
|
|
587
|
+
if (value !== undefined &&
|
|
588
|
+
(typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 2_147_483_647))
|
|
589
|
+
throw new RangeError(`${name} must be a positive finite timer duration`);
|
|
590
|
+
}
|
|
591
|
+
const LOST = { wasClean: false, code: 1006, reason: "Connection lost" };
|
|
592
|
+
/** A 4xx other than 409 and 429 will not get better by retrying. */
|
|
593
|
+
const fatal = (error) => error instanceof RuntimeError &&
|
|
594
|
+
error.status >= 400 &&
|
|
595
|
+
error.status < 500 &&
|
|
596
|
+
error.status !== 409 &&
|
|
597
|
+
error.status !== 429;
|
|
598
|
+
/** A stable handle across reattachments; input is never retried or buffered.
|
|
599
|
+
* Each connection is a Runtime terminal attached to the session's process. */
|
|
569
600
|
export class PtySession {
|
|
570
|
-
terminal;
|
|
571
601
|
sandbox;
|
|
572
602
|
processId;
|
|
573
603
|
sessionId;
|
|
574
604
|
slug;
|
|
575
605
|
created;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
606
|
+
events;
|
|
607
|
+
#state = "connecting";
|
|
608
|
+
#readyState = 0;
|
|
609
|
+
#terminal;
|
|
610
|
+
#generation = 0;
|
|
611
|
+
#attempt = 0;
|
|
612
|
+
#deadline = 0;
|
|
613
|
+
#retry;
|
|
614
|
+
#stopped = false;
|
|
615
|
+
#exited = false;
|
|
616
|
+
#fatal = false;
|
|
617
|
+
#established = false;
|
|
618
|
+
#closeReported = false;
|
|
619
|
+
#dimensions;
|
|
620
|
+
#removeWake;
|
|
621
|
+
#resolveReady;
|
|
622
|
+
#rejectReady;
|
|
623
|
+
/** Resolves once the first connection is up. */
|
|
624
|
+
ready;
|
|
625
|
+
constructor(sandbox, processId, sessionId, slug, created, events, dimensions) {
|
|
579
626
|
this.sandbox = sandbox;
|
|
580
627
|
this.processId = processId;
|
|
581
628
|
this.sessionId = sessionId;
|
|
582
629
|
this.slug = slug;
|
|
583
630
|
this.created = created;
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
631
|
+
this.events = events;
|
|
632
|
+
this.#dimensions = dimensions;
|
|
633
|
+
this.ready = new Promise((resolve, reject) => {
|
|
634
|
+
this.#resolveReady = resolve;
|
|
635
|
+
this.#rejectReady = reject;
|
|
636
|
+
});
|
|
637
|
+
void this.ready.catch(() => { });
|
|
638
|
+
if (events.reconnect && typeof window !== "undefined" && typeof document !== "undefined") {
|
|
639
|
+
// A browser back online or back in view retries a dropped session now.
|
|
640
|
+
const wake = () => {
|
|
641
|
+
if (document.visibilityState !== "hidden" && this.#state === "closed")
|
|
642
|
+
this.reconnect();
|
|
643
|
+
};
|
|
644
|
+
window.addEventListener("online", wake);
|
|
645
|
+
window.addEventListener("pageshow", wake);
|
|
646
|
+
document.addEventListener("visibilitychange", wake);
|
|
647
|
+
this.#removeWake = () => {
|
|
648
|
+
window.removeEventListener("online", wake);
|
|
649
|
+
window.removeEventListener("pageshow", wake);
|
|
650
|
+
document.removeEventListener("visibilitychange", wake);
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
void this.#dial();
|
|
654
|
+
}
|
|
655
|
+
#setState(state) {
|
|
656
|
+
if (this.#state === state)
|
|
657
|
+
return;
|
|
658
|
+
this.#state = state;
|
|
659
|
+
this.events.onStateChange?.(state);
|
|
660
|
+
}
|
|
661
|
+
async #dial() {
|
|
662
|
+
const generation = ++this.#generation;
|
|
663
|
+
const current = () => generation === this.#generation && !this.#stopped;
|
|
664
|
+
const limit = this.#established
|
|
665
|
+
? Math.min(this.events.connectTimeoutMs ?? 15_000, Math.max(1, this.#deadline - Date.now()))
|
|
666
|
+
: this.events.connectTimeoutMs;
|
|
667
|
+
try {
|
|
668
|
+
// Before a reattachment, as Freestyle's preflight: a session that is
|
|
669
|
+
// gone, or credentials that no longer work, end it instead of retrying.
|
|
670
|
+
if (this.#established)
|
|
671
|
+
await this.sandbox.processes.get(this.processId);
|
|
672
|
+
if (!current())
|
|
673
|
+
return;
|
|
674
|
+
const opening = this.sandbox.terminal({
|
|
675
|
+
processId: this.processId,
|
|
676
|
+
onData: (data) => {
|
|
677
|
+
if (current())
|
|
678
|
+
this.events.onData?.(data);
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
let timer;
|
|
682
|
+
const terminal = await (limit === undefined
|
|
683
|
+
? opening
|
|
684
|
+
: Promise.race([
|
|
685
|
+
opening,
|
|
686
|
+
new Promise((_, reject) => {
|
|
687
|
+
timer = setTimeout(() => {
|
|
688
|
+
void opening.then((late) => late.close()).catch(() => { });
|
|
689
|
+
reject(new Error("PTY handshake timed out"));
|
|
690
|
+
}, limit);
|
|
691
|
+
}),
|
|
692
|
+
]).finally(() => clearTimeout(timer)));
|
|
693
|
+
if (!current()) {
|
|
694
|
+
terminal.close();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
this.#bind(terminal, generation);
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
if (!current())
|
|
701
|
+
return;
|
|
702
|
+
if (!this.#established)
|
|
703
|
+
return this.#finish(translate(error));
|
|
704
|
+
if (fatal(error)) {
|
|
705
|
+
this.#fatal = true;
|
|
706
|
+
this.#finish(translate(error));
|
|
707
|
+
}
|
|
708
|
+
else
|
|
709
|
+
this.#scheduleRetry(translate(error));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
#bind(terminal, generation) {
|
|
713
|
+
this.#terminal = terminal;
|
|
714
|
+
this.#readyState = 1;
|
|
715
|
+
const recovering = this.#established;
|
|
716
|
+
this.#established = true;
|
|
717
|
+
this.#attempt = 0;
|
|
718
|
+
this.#deadline = 0;
|
|
719
|
+
this.#closeReported = false;
|
|
720
|
+
this.#resolveReady();
|
|
721
|
+
this.#setState("connected");
|
|
722
|
+
if (recovering && this.#dimensions)
|
|
723
|
+
terminal.resize(this.#dimensions.cols, this.#dimensions.rows);
|
|
724
|
+
void terminal.exited.then((exitCode) => this.#ended(generation, exitCode));
|
|
725
|
+
}
|
|
726
|
+
/** The connection ended: with the shell's exit code, or without one when the
|
|
727
|
+
* shell was killed by a signal or the connection dropped. */
|
|
728
|
+
#ended(generation, exitCode) {
|
|
729
|
+
const current = () => generation === this.#generation && !this.#stopped;
|
|
730
|
+
if (!current())
|
|
731
|
+
return;
|
|
732
|
+
this.#readyState = 3;
|
|
733
|
+
if (exitCode !== null)
|
|
734
|
+
return this.#exit(exitCode);
|
|
735
|
+
this.sandbox.processes.get(this.processId).then((process) => {
|
|
736
|
+
if (!current())
|
|
737
|
+
return;
|
|
738
|
+
if (process.state === "running")
|
|
739
|
+
this.#lost();
|
|
740
|
+
else
|
|
741
|
+
this.#exit(process.exitCode);
|
|
742
|
+
}, (error) => {
|
|
743
|
+
if (current())
|
|
744
|
+
this.#lost(translate(error));
|
|
589
745
|
});
|
|
590
|
-
this.onError = events.onError;
|
|
591
746
|
}
|
|
592
|
-
|
|
747
|
+
#retire() {
|
|
748
|
+
++this.#generation;
|
|
749
|
+
clearTimeout(this.#retry);
|
|
750
|
+
if (this.#readyState === 1)
|
|
751
|
+
this.#readyState = 2;
|
|
752
|
+
this.#terminal?.close();
|
|
753
|
+
this.#terminal = undefined;
|
|
754
|
+
}
|
|
755
|
+
#exit(exitCode) {
|
|
756
|
+
this.#exited = true;
|
|
757
|
+
this.#retire();
|
|
758
|
+
this.#readyState = 3;
|
|
759
|
+
this.#setState("exited");
|
|
760
|
+
this.#removeWake?.();
|
|
761
|
+
if (exitCode !== null)
|
|
762
|
+
this.events.onExit?.(exitCode);
|
|
763
|
+
this.#report({ wasClean: true, code: 1000, reason: "" });
|
|
764
|
+
}
|
|
765
|
+
#lost(error) {
|
|
766
|
+
this.#retire();
|
|
767
|
+
this.#readyState = 3;
|
|
768
|
+
if (this.#stopped)
|
|
769
|
+
return;
|
|
770
|
+
if (!this.events.reconnect || this.#exited || this.#fatal)
|
|
771
|
+
return this.#finish(error);
|
|
772
|
+
this.#scheduleRetry(error);
|
|
773
|
+
}
|
|
774
|
+
#scheduleRetry(error) {
|
|
775
|
+
const options = typeof this.events.reconnect === "object" ? this.events.reconnect : null;
|
|
776
|
+
this.#deadline ||= Date.now() + (options?.timeoutMs ?? 300_000);
|
|
777
|
+
if (Date.now() >= this.#deadline)
|
|
778
|
+
return this.#finish(error ?? new Error("PTY reconnect timed out"));
|
|
779
|
+
this.#setState("reconnecting");
|
|
780
|
+
const base = Math.min((options?.initialDelayMs ?? 400) * 2 ** this.#attempt++, options?.maxDelayMs ?? 10_000);
|
|
781
|
+
const delay = Math.min(Math.round(base * (0.7 + Math.random() * 0.6)), Math.max(0, this.#deadline - Date.now()));
|
|
782
|
+
this.#retry = setTimeout(() => void this.#dial(), delay);
|
|
783
|
+
}
|
|
784
|
+
#finish(error, info = LOST) {
|
|
785
|
+
clearTimeout(this.#retry);
|
|
786
|
+
if (!this.#established)
|
|
787
|
+
this.#rejectReady(error ?? new Error("PTY connection failed"));
|
|
788
|
+
this.#setState(this.#exited ? "exited" : "closed");
|
|
789
|
+
if (this.#fatal || this.#exited || !this.#established)
|
|
790
|
+
this.#removeWake?.();
|
|
791
|
+
this.#report(info, error);
|
|
792
|
+
}
|
|
793
|
+
#report(info, error) {
|
|
794
|
+
if (this.#closeReported)
|
|
795
|
+
return;
|
|
796
|
+
this.#closeReported = true;
|
|
797
|
+
if (error)
|
|
798
|
+
this.events.onError?.(error);
|
|
799
|
+
this.events.onClose?.(info);
|
|
800
|
+
}
|
|
801
|
+
/** Retry now after a network change or a spent retry budget. Never opens a new shell. */
|
|
802
|
+
reconnect() {
|
|
803
|
+
if (this.#stopped || this.#exited || this.#fatal || !this.#established)
|
|
804
|
+
return;
|
|
805
|
+
this.#retire();
|
|
806
|
+
this.#attempt = 0;
|
|
807
|
+
this.#deadline = 0;
|
|
808
|
+
this.#closeReported = false;
|
|
809
|
+
this.#scheduleRetry();
|
|
810
|
+
}
|
|
811
|
+
/** Send input once. Throws while disconnected; uncertain keystrokes are never replayed. */
|
|
593
812
|
write(data) {
|
|
594
|
-
this
|
|
813
|
+
if (this.#state !== "connected" || !this.#terminal)
|
|
814
|
+
throw new Error("PTY is not connected");
|
|
815
|
+
this.#terminal.write(data);
|
|
595
816
|
}
|
|
817
|
+
/** Remembers the size and applies it again after a reattachment. */
|
|
596
818
|
resize(options) {
|
|
597
|
-
|
|
819
|
+
const cols = dimension(options.cols, 80, "cols");
|
|
820
|
+
const rows = dimension(options.rows, 24, "rows");
|
|
821
|
+
this.#dimensions = { cols, rows };
|
|
822
|
+
if (this.#state === "connected")
|
|
823
|
+
this.#terminal?.resize(cols, rows);
|
|
598
824
|
}
|
|
599
825
|
signal(sig) {
|
|
600
826
|
if (sig !== "sigint" && sig !== "sigkill")
|
|
601
827
|
throw new TypeError('signal must be "sigint" or "sigkill"');
|
|
828
|
+
if (this.#state !== "connected")
|
|
829
|
+
throw new Error("PTY is not connected");
|
|
602
830
|
void (async () => {
|
|
603
831
|
const process = await this.sandbox.processes.get(this.processId);
|
|
604
832
|
await process.kill(sig === "sigint" ? "SIGINT" : "SIGKILL");
|
|
605
|
-
})().catch((error) => this.onError?.(error));
|
|
833
|
+
})().catch((error) => this.events.onError?.(translate(error)));
|
|
606
834
|
}
|
|
607
|
-
/**
|
|
835
|
+
/** Cancels recovery and closes the connection; the shell keeps running and
|
|
836
|
+
* attach() finds it. */
|
|
608
837
|
detach() {
|
|
609
|
-
this.#
|
|
610
|
-
|
|
838
|
+
if (this.#stopped)
|
|
839
|
+
return;
|
|
840
|
+
this.#stopped = true;
|
|
841
|
+
this.#retire();
|
|
842
|
+
this.#removeWake?.();
|
|
843
|
+
this.#finish(undefined, { wasClean: true, code: 1000, reason: "Detached" });
|
|
611
844
|
}
|
|
612
845
|
get readyState() {
|
|
613
846
|
return this.#readyState;
|
|
614
847
|
}
|
|
848
|
+
get state() {
|
|
849
|
+
return this.#state;
|
|
850
|
+
}
|
|
615
851
|
}
|
|
616
852
|
export class VmPty {
|
|
617
853
|
get;
|
|
@@ -633,12 +869,14 @@ export class VmPty {
|
|
|
633
869
|
throw notFound(`PTY session ${selector} of ${linuxUser}`);
|
|
634
870
|
return found;
|
|
635
871
|
}
|
|
636
|
-
async connect(sandbox, found, created, events) {
|
|
637
|
-
const
|
|
638
|
-
|
|
872
|
+
async connect(sandbox, found, created, events, dimensions) {
|
|
873
|
+
const session = new PtySession(sandbox, found.processId, found.info.sessionId, found.info.slug ?? undefined, created, events, dimensions);
|
|
874
|
+
await session.ready;
|
|
875
|
+
return session;
|
|
639
876
|
}
|
|
640
877
|
async open(options = {}) {
|
|
641
878
|
only("Freestyle pty open", options, PTY_OPEN);
|
|
879
|
+
connectionOptions(options);
|
|
642
880
|
const cols = dimension(options.cols, 80, "cols");
|
|
643
881
|
const rows = dimension(options.rows, 24, "rows");
|
|
644
882
|
const user = options.linuxUser === undefined ? "" : validateLinuxUser(options.linuxUser);
|
|
@@ -654,7 +892,7 @@ export class VmPty {
|
|
|
654
892
|
const existing = (await this.sessions(sandbox)).find((s) => s.info.slug === options.slug);
|
|
655
893
|
// Get-or-create: a running session with the slug is handed back, exec unrun.
|
|
656
894
|
if (existing)
|
|
657
|
-
return this.connect(sandbox, existing, false, options);
|
|
895
|
+
return this.connect(sandbox, existing, false, options, { cols, rows });
|
|
658
896
|
}
|
|
659
897
|
const shell = options.exec === undefined
|
|
660
898
|
? user
|
|
@@ -667,18 +905,12 @@ export class VmPty {
|
|
|
667
905
|
const started = session(process.toJSON());
|
|
668
906
|
if (!started)
|
|
669
907
|
throw new Error("The started PTY session has no session line");
|
|
670
|
-
return this.connect(sandbox, started, true, options);
|
|
908
|
+
return this.connect(sandbox, started, true, options, { cols, rows });
|
|
671
909
|
});
|
|
672
910
|
}
|
|
673
911
|
async attach(options) {
|
|
674
|
-
only("Freestyle pty attach", options, [
|
|
675
|
-
|
|
676
|
-
"linuxUser",
|
|
677
|
-
"onData",
|
|
678
|
-
"onExit",
|
|
679
|
-
"onClose",
|
|
680
|
-
"onError",
|
|
681
|
-
]);
|
|
912
|
+
only("Freestyle pty attach", options, ["session", "linuxUser", ...PTY_EVENTS]);
|
|
913
|
+
connectionOptions(options);
|
|
682
914
|
if (options.linuxUser !== undefined)
|
|
683
915
|
validateLinuxUser(options.linuxUser);
|
|
684
916
|
return api(async () => {
|
|
@@ -732,6 +964,106 @@ export class VmLinuxUserPty {
|
|
|
732
964
|
return this.pty.close(session, { linuxUser: this.linuxUser });
|
|
733
965
|
}
|
|
734
966
|
}
|
|
967
|
+
/** Terminal queries a replay may hold (device attributes, status and mode
|
|
968
|
+
* reports, window ops): answering them again would type the answers. */
|
|
969
|
+
const XTERM_QUERIES = [
|
|
970
|
+
{ final: "c" },
|
|
971
|
+
{ prefix: ">", final: "c" },
|
|
972
|
+
{ final: "n" },
|
|
973
|
+
{ prefix: "?", final: "n" },
|
|
974
|
+
{ final: "t" },
|
|
975
|
+
{ intermediates: "$", final: "p" },
|
|
976
|
+
{ prefix: "?", intermediates: "$", final: "p" },
|
|
977
|
+
];
|
|
978
|
+
/** Freestyle 0.2.18's connectXterm: xterm.js on a named session that
|
|
979
|
+
* reattaches by itself unless `reconnect: false`. Each connection replays the
|
|
980
|
+
* session's output from its start, so the screen is reset first and the
|
|
981
|
+
* replay's terminal queries go unanswered until it has been written (150ms
|
|
982
|
+
* quiet, at most 2 seconds). dispose() detaches; the shell keeps running. */
|
|
983
|
+
export async function connectXterm({ pty, terminal, ...options }) {
|
|
984
|
+
only("Freestyle connectXterm", options, PTY_OPEN);
|
|
985
|
+
let muted = [];
|
|
986
|
+
let quiet;
|
|
987
|
+
let cap;
|
|
988
|
+
let replay = 0;
|
|
989
|
+
let disposed = false;
|
|
990
|
+
const unmute = () => {
|
|
991
|
+
clearTimeout(quiet);
|
|
992
|
+
clearTimeout(cap);
|
|
993
|
+
for (const handler of muted)
|
|
994
|
+
handler.dispose();
|
|
995
|
+
muted = [];
|
|
996
|
+
};
|
|
997
|
+
const endReplay = () => {
|
|
998
|
+
clearTimeout(quiet);
|
|
999
|
+
clearTimeout(cap);
|
|
1000
|
+
const current = replay;
|
|
1001
|
+
// Unmute once xterm has parsed everything written before this point.
|
|
1002
|
+
terminal.write("", () => {
|
|
1003
|
+
if (current === replay)
|
|
1004
|
+
unmute();
|
|
1005
|
+
});
|
|
1006
|
+
};
|
|
1007
|
+
const beginReplay = () => {
|
|
1008
|
+
++replay;
|
|
1009
|
+
unmute();
|
|
1010
|
+
terminal.write("\x1bc");
|
|
1011
|
+
muted = XTERM_QUERIES.map((id) => terminal.parser.registerCsiHandler(id, () => true));
|
|
1012
|
+
muted.push(terminal.parser.registerDcsHandler({ intermediates: "$", final: "q" }, () => true));
|
|
1013
|
+
quiet = setTimeout(endReplay, 150);
|
|
1014
|
+
cap = setTimeout(endReplay, 2_000);
|
|
1015
|
+
};
|
|
1016
|
+
const stopReplay = () => {
|
|
1017
|
+
++replay;
|
|
1018
|
+
unmute();
|
|
1019
|
+
};
|
|
1020
|
+
try {
|
|
1021
|
+
const session = await pty.open({
|
|
1022
|
+
...options,
|
|
1023
|
+
reconnect: options.reconnect ?? true,
|
|
1024
|
+
cols: options.cols ?? terminal.cols,
|
|
1025
|
+
rows: options.rows ?? terminal.rows,
|
|
1026
|
+
onStateChange(state) {
|
|
1027
|
+
if (state === "connected")
|
|
1028
|
+
beginReplay();
|
|
1029
|
+
if (state === "closed" || state === "exited")
|
|
1030
|
+
endReplay();
|
|
1031
|
+
options.onStateChange?.(state);
|
|
1032
|
+
},
|
|
1033
|
+
onData(bytes) {
|
|
1034
|
+
if (disposed)
|
|
1035
|
+
return;
|
|
1036
|
+
if (muted.length) {
|
|
1037
|
+
clearTimeout(quiet);
|
|
1038
|
+
quiet = setTimeout(endReplay, 150);
|
|
1039
|
+
}
|
|
1040
|
+
terminal.write(bytes);
|
|
1041
|
+
options.onData?.(bytes);
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
const input = terminal.onData((data) => {
|
|
1045
|
+
if (session.state === "connected")
|
|
1046
|
+
session.write(data);
|
|
1047
|
+
});
|
|
1048
|
+
const resize = terminal.onResize((size) => session.resize(size));
|
|
1049
|
+
return {
|
|
1050
|
+
session,
|
|
1051
|
+
dispose() {
|
|
1052
|
+
if (disposed)
|
|
1053
|
+
return;
|
|
1054
|
+
disposed = true;
|
|
1055
|
+
input.dispose();
|
|
1056
|
+
resize.dispose();
|
|
1057
|
+
session.detach();
|
|
1058
|
+
stopReplay();
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
stopReplay();
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
735
1067
|
export class VmLinuxUser {
|
|
736
1068
|
vm;
|
|
737
1069
|
linuxUser;
|
package/dist/identity.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
2
|
import { sandboxMarker } from "./transport.js";
|
|
3
|
+
/** The guest's environment file, which holds the sandbox's current request
|
|
4
|
+
* token: the guest writes it again when the token changes (a wake on another
|
|
5
|
+
* server, a restore), while a process keeps the environment it started with. */
|
|
6
|
+
async function fileCredentials() {
|
|
7
|
+
try {
|
|
8
|
+
// Loaded here, so the SDK still loads where there is no file system.
|
|
9
|
+
const { readFile } = await import("node:fs/promises");
|
|
10
|
+
const env = JSON.parse(await readFile(sandboxMarker.path, "utf8"));
|
|
11
|
+
return { url: env.RUNTIME_ID_TOKEN_REQUEST_URL, token: env.RUNTIME_ID_TOKEN_REQUEST_TOKEN };
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
async function requestCredentials() {
|
|
4
18
|
let url = process.env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
5
19
|
let token = process.env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
6
20
|
if (!url || !token) {
|
|
7
21
|
// A process started before the guest wrote its environment still finds it.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const env = JSON.parse(await readFile(sandboxMarker.path, "utf8"));
|
|
12
|
-
url = env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
13
|
-
token = env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
22
|
+
const file = await fileCredentials();
|
|
23
|
+
url = file?.url;
|
|
24
|
+
token = file?.token;
|
|
18
25
|
}
|
|
19
26
|
return url && token ? { url, token } : null;
|
|
20
27
|
}
|
|
@@ -37,14 +44,25 @@ export async function identityToken(input) {
|
|
|
37
44
|
status: 0,
|
|
38
45
|
message: "Identity tokens are issued only inside a Runtime sandbox (RUNTIME_ID_TOKEN_REQUEST_TOKEN is not set).",
|
|
39
46
|
});
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const ask = (from) => {
|
|
48
|
+
const url = new URL(from.url);
|
|
49
|
+
url.searchParams.set("audience", input.audience);
|
|
50
|
+
if (input.lifetimeSeconds !== undefined)
|
|
51
|
+
url.searchParams.set("lifetimeSeconds", String(input.lifetimeSeconds));
|
|
52
|
+
return fetch(url, {
|
|
53
|
+
headers: { authorization: `Bearer ${from.token}` },
|
|
54
|
+
signal: input.signal ?? AbortSignal.timeout(10_000),
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
let response = await ask(found);
|
|
58
|
+
// Refused for a request token that has since changed: a long-running
|
|
59
|
+
// process asks again with the current one from the guest's file (the hub's
|
|
60
|
+
// own, 16 October 2026, from its start until it was restarted).
|
|
61
|
+
if (response.status === 401 || response.status === 403) {
|
|
62
|
+
const file = await fileCredentials();
|
|
63
|
+
if (file?.url && file.token && file.token !== found.token)
|
|
64
|
+
response = await ask({ url: file.url, token: file.token });
|
|
65
|
+
}
|
|
48
66
|
const body = (await response.json().catch(() => ({})));
|
|
49
67
|
if (!response.ok || !body.token)
|
|
50
68
|
throw new RuntimeError({
|
package/dist/index.d.ts
CHANGED
|
@@ -9,14 +9,14 @@ import { type CreateOptions, type RuntimeOptions } from "./client.js";
|
|
|
9
9
|
import { Sandbox as SandboxClass } from "./sandbox.js";
|
|
10
10
|
import type { CreateSandbox, KeepAliveOptions } from "./types.js";
|
|
11
11
|
import type { RequestOptions } from "./transport.js";
|
|
12
|
-
export { Runtime, Sandboxes, FeedbackApi, SupportApi, type CreateOptions, type RuntimeOptions, type SupportReply, } from "./client.js";
|
|
12
|
+
export { Runtime, Sandboxes, FeedbackApi, SupportApi, type CreateOptions, type LifecycleOptions, type RuntimeOptions, type SupportReply, } from "./client.js";
|
|
13
13
|
export { Files, Process, Processes, SandboxSessions, Terminal, type CreatedSandboxSession, type SandboxSession, } from "./sandbox.js";
|
|
14
14
|
export { Tunnel, TunnelStream, type PortForward } from "./tunnel.js";
|
|
15
15
|
export { Page } from "./page.js";
|
|
16
|
-
export type { KeyLimits } from "./products/limits.js";
|
|
16
|
+
export type { AccountLimit, KeyLimits } from "./products/limits.js";
|
|
17
17
|
export type { AuditEvent, AuditPage } from "./products/audit.js";
|
|
18
18
|
export type { UsageExportQuery, UsageExportRow, UsageExportPage, } from "./products/observability.js";
|
|
19
|
-
export type { JobSecret, Secret, SecretRule, SecretUse, SetSecret } from "./products/secrets.js";
|
|
19
|
+
export type { JobSecret, Secret, SecretRule, SecretScope, SecretUse, SetSecret, } from "./products/secrets.js";
|
|
20
20
|
export { SecretPartlyStoredError } from "./products/secrets.js";
|
|
21
21
|
export type { McpCatalogEntry, McpGateway, McpServerRequest } from "./products/mcp.js";
|
|
22
22
|
export type { FileEvent, FileEventType, WatchHandle, WatchNotice, WatchOptions, } from "./products/watch.js";
|
|
@@ -74,4 +74,5 @@ export type { RuntimeOptions as ClientOptions };
|
|
|
74
74
|
export { identityToken, type IdentityToken } from "./identity.js";
|
|
75
75
|
export { openWalletAccount, claimWalletAccount, type OpenedWalletAccount, type WalletClaim, type WalletOptions, } from "./wallet.js";
|
|
76
76
|
export type { Topup } from "./products/billing.js";
|
|
77
|
+
export type { NewKey, MadeKey, ListedKey, KeyUsage } from "./products/api-keys.js";
|
|
77
78
|
export type { SsoConnection, SsoStatus } from "./products/sso.js";
|