withruntime 0.11.2 → 0.12.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 +2 -2
- package/COMPATIBILITY.md +74 -9
- package/DAYTONA.md +33 -5
- package/E2B.md +85 -24
- package/README.md +9 -8
- package/README.src.md +8 -7
- package/VERCEL.md +43 -5
- package/compatibility-lock.json +58 -49
- package/dist/billing-cli.d.ts +1 -1
- package/dist/billing-cli.js +77 -1
- package/dist/blaxel/errors.d.ts +14 -0
- package/dist/blaxel/errors.js +29 -0
- package/dist/blaxel/index.d.ts +2 -2
- package/dist/blaxel/index.js +4 -4
- package/dist/blaxel/interpreter.d.ts +2 -5
- package/dist/blaxel/sandbox.d.ts +14 -5
- package/dist/blaxel/sandbox.js +63 -7
- package/dist/blaxel/snapshot.d.ts +4 -0
- package/dist/blaxel/snapshot.js +4 -0
- package/dist/cli/logs.d.ts +21 -0
- package/dist/cli/logs.js +188 -0
- package/dist/cli/logs.test.d.ts +1 -0
- package/dist/cli/logs.test.js +272 -0
- package/dist/cli-extras.d.ts +3 -3
- package/dist/cli-extras.js +17 -12
- package/dist/cli-words.d.ts +10 -1
- package/dist/cli-words.js +18 -0
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +405 -204
- package/dist/client.d.ts +2 -4
- package/dist/client.js +27 -11
- package/dist/cloudflare/errors.d.ts +143 -0
- package/dist/cloudflare/errors.js +167 -0
- package/dist/cloudflare/index.d.ts +175 -6
- package/dist/cloudflare/index.js +416 -26
- package/dist/cloudflare/interpreter.d.ts +119 -0
- package/dist/cloudflare/interpreter.js +223 -0
- package/dist/cloudflare/streams.d.ts +38 -0
- package/dist/cloudflare/streams.js +145 -0
- package/dist/cloudflare/watch.d.ts +9 -0
- package/dist/cloudflare/watch.js +24 -0
- package/dist/codesandbox/index.d.ts +216 -28
- package/dist/codesandbox/index.js +478 -78
- package/dist/compat/core.d.ts +18 -0
- package/dist/compat/core.js +29 -0
- package/dist/compat/files.d.ts +1 -0
- package/dist/compat/files.js +25 -5
- package/dist/daytona/computer-use.d.ts +13 -4
- package/dist/daytona/computer-use.js +66 -8
- package/dist/daytona/daytona.d.ts +6 -0
- package/dist/daytona/daytona.js +30 -3
- package/dist/daytona/errors.d.ts +11 -0
- package/dist/daytona/errors.js +18 -0
- package/dist/daytona/filesystem.js +2 -1
- package/dist/daytona/image.d.ts +13 -9
- package/dist/daytona/image.js +18 -12
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/sandbox.d.ts +8 -2
- package/dist/daytona/sandbox.js +16 -2
- package/dist/domains/index.d.ts +9 -0
- package/dist/domains/index.js +10 -0
- package/dist/e2b/commands.d.ts +1 -1
- package/dist/e2b/commands.js +6 -7
- package/dist/e2b/errors.d.ts +17 -0
- package/dist/e2b/errors.js +40 -1
- package/dist/e2b/index.d.ts +2 -2
- package/dist/e2b/index.js +1 -1
- package/dist/e2b/sandbox.d.ts +27 -21
- package/dist/e2b/sandbox.js +34 -8
- package/dist/e2b/unsupported.js +1 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +16 -4
- package/dist/freestyle/index.d.ts +285 -37
- package/dist/freestyle/index.js +656 -144
- package/dist/identity.d.ts +3 -0
- package/dist/identity.js +27 -0
- package/dist/images/index.d.ts +9 -0
- package/dist/images/index.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/ips/index.d.ts +9 -0
- package/dist/ips/index.js +10 -0
- package/dist/jobs/index.d.ts +9 -0
- package/dist/jobs/index.js +10 -0
- package/dist/keys.js +2 -0
- package/dist/login.js +2 -0
- package/dist/machine.d.ts +12 -3
- package/dist/machine.js +20 -3
- package/dist/mcp.js +1 -1
- package/dist/modal/index.d.ts +339 -61
- package/dist/modal/index.js +781 -121
- package/dist/network-cli.d.ts +3 -1
- package/dist/network-cli.js +78 -36
- package/dist/networks/index.d.ts +11 -0
- package/dist/networks/index.js +10 -0
- package/dist/ports/index.d.ts +9 -0
- package/dist/ports/index.js +10 -0
- package/dist/product-names.d.ts +52 -0
- package/dist/product-names.js +164 -0
- package/dist/product-renames.d.ts +103 -0
- package/dist/product-renames.js +189 -0
- package/dist/products/apps.d.ts +176 -0
- package/dist/products/apps.js +165 -0
- package/dist/products/billing.d.ts +41 -0
- package/dist/products/billing.js +13 -0
- package/dist/products/browser.d.ts +10 -0
- package/dist/products/browsers.d.ts +71 -0
- package/dist/products/browsers.js +102 -0
- package/dist/products/builds.d.ts +126 -0
- package/dist/products/builds.js +221 -0
- package/dist/products/index.d.ts +3 -16
- package/dist/products/index.js +3 -14
- package/dist/products/jobs.d.ts +17 -0
- package/dist/products/jobs.js +27 -10
- package/dist/products/logs.d.ts +245 -0
- package/dist/products/logs.js +274 -0
- package/dist/products/logs.test.d.ts +1 -0
- package/dist/products/logs.test.js +343 -0
- package/dist/products/network-products.d.ts +38 -7
- package/dist/products/network-products.js +31 -18
- package/dist/products/observability.d.ts +3 -2
- package/dist/products/private-network.js +1 -1
- package/dist/products/queues.d.ts +149 -0
- package/dist/products/queues.js +213 -0
- package/dist/products/referrals.d.ts +21 -7
- package/dist/products/referrals.js +3 -3
- package/dist/products/secrets.d.ts +7 -3
- package/dist/products/secrets.js +39 -18
- package/dist/products/signed-requests.d.ts +21 -0
- package/dist/products/signed-requests.js +11 -0
- package/dist/products/streams.d.ts +73 -0
- package/dist/products/streams.js +17 -0
- package/dist/products/switching.d.ts +35 -7
- package/dist/products/switching.js +4 -3
- package/dist/products/volumes.d.ts +3 -0
- package/dist/record-cli.d.ts +23 -0
- package/dist/record-cli.js +308 -0
- package/dist/renamed-products.d.ts +38 -0
- package/dist/renamed-products.js +34 -0
- package/dist/runloop/index.d.ts +9 -2
- package/dist/runloop/index.js +29 -8
- package/dist/runloop/sdk.d.ts +131 -0
- package/dist/runloop/sdk.js +167 -0
- package/dist/sandbox.d.ts +3 -2
- package/dist/sandbox.js +14 -9
- package/dist/sandboxes/index.d.ts +11 -0
- package/dist/sandboxes/index.js +12 -0
- package/dist/snapshots/index.d.ts +9 -0
- package/dist/snapshots/index.js +11 -0
- package/dist/snapshots.d.ts +7 -4
- package/dist/sprites/index.d.ts +343 -31
- package/dist/sprites/index.js +866 -84
- package/dist/ssh.d.ts +1 -1
- package/dist/ssh.js +11 -11
- package/dist/terminal.d.ts +24 -0
- package/dist/terminal.js +32 -0
- package/dist/transport.d.ts +15 -1
- package/dist/transport.js +56 -3
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +63 -0
- package/dist/vercel/sandbox.js +5 -3
- package/dist/vercel/snapshot.js +10 -2
- package/dist/volumes/index.d.ts +9 -0
- package/dist/volumes/index.js +10 -0
- package/package.json +48 -2
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { Transport } from "../transport.js";
|
|
3
|
+
import { InvalidRequestError, NotFoundError, RuntimeError } from "../errors.js";
|
|
4
|
+
import { alerts, logs, traces, } from "./logs.js";
|
|
5
|
+
const line = {
|
|
6
|
+
time: "2026-10-06T12:00:00.000000001Z",
|
|
7
|
+
timeUnixNano: "1791288000000000001",
|
|
8
|
+
severity: 9,
|
|
9
|
+
severityText: "INFO",
|
|
10
|
+
body: "same α",
|
|
11
|
+
labels: { app: "api" },
|
|
12
|
+
attributes: {},
|
|
13
|
+
traceId: null,
|
|
14
|
+
spanId: null,
|
|
15
|
+
};
|
|
16
|
+
const view = { name: "errors", labels: {}, query: '{app="api"}' };
|
|
17
|
+
const rule = { ...view, condition: "above 5", for: "5m", actions: [] };
|
|
18
|
+
const options = { idempotencyKey: "repeat-this-write" };
|
|
19
|
+
function fixture(handler) {
|
|
20
|
+
const requests = [];
|
|
21
|
+
const transport = new Transport({
|
|
22
|
+
apiKey: "test-only",
|
|
23
|
+
baseUrl: "https://fake.test",
|
|
24
|
+
maxRetries: 0,
|
|
25
|
+
fetch: (async (input, init) => {
|
|
26
|
+
const url = new URL(input instanceof Request ? input.url : input);
|
|
27
|
+
requests.push({ url, init: init ?? {} });
|
|
28
|
+
return handler(url, init ?? {});
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
client: { logs: logs(transport), traces: traces(transport), alerts: alerts(transport) },
|
|
33
|
+
requests,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const calls = [
|
|
37
|
+
{
|
|
38
|
+
name: "search",
|
|
39
|
+
method: "POST",
|
|
40
|
+
path: "/v1/logs:search",
|
|
41
|
+
body: { query: "{}", cursor: "c", limit: 2 },
|
|
42
|
+
run: (c) => c.logs.search({ query: "{}", cursor: "c", limit: 2 }, options),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "instant query",
|
|
46
|
+
method: "GET",
|
|
47
|
+
path: "/v1/logs/query",
|
|
48
|
+
query: { query: "{}", time: "42", direction: "forward", limit: "2" },
|
|
49
|
+
run: (c) => c.logs.query({ query: "{}", time: "42", direction: "forward", limit: 2 }),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "range query",
|
|
53
|
+
method: "GET",
|
|
54
|
+
path: "/v1/logs/query_range",
|
|
55
|
+
query: { query: "{}", start: "1", end: "2", step: "1s" },
|
|
56
|
+
run: (c) => c.logs.queryRange({ query: "{}", start: "1", end: "2", step: "1s" }),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "labels",
|
|
60
|
+
method: "GET",
|
|
61
|
+
path: "/v1/logs/labels",
|
|
62
|
+
query: { start: "1", end: "2" },
|
|
63
|
+
run: (c) => c.logs.labels({ start: "1", end: "2" }),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "label values",
|
|
67
|
+
method: "GET",
|
|
68
|
+
path: "/v1/logs/labels/a%2Fb/values",
|
|
69
|
+
run: (c) => c.logs.labelValues("a/b"),
|
|
70
|
+
},
|
|
71
|
+
{ name: "trace get", method: "GET", path: "/v1/traces/a%2Fb", run: (c) => c.traces.get("a/b") },
|
|
72
|
+
{
|
|
73
|
+
name: "trace search",
|
|
74
|
+
method: "GET",
|
|
75
|
+
path: "/v1/traces:search",
|
|
76
|
+
query: { q: "{duration > 1s}" },
|
|
77
|
+
run: (c) => c.traces.search("{duration > 1s}"),
|
|
78
|
+
},
|
|
79
|
+
{ name: "trace tags", method: "GET", path: "/v1/traces/tags", run: (c) => c.traces.tags() },
|
|
80
|
+
{
|
|
81
|
+
name: "log ingest",
|
|
82
|
+
method: "POST",
|
|
83
|
+
path: "/v1/logs",
|
|
84
|
+
body: { resourceLogs: [] },
|
|
85
|
+
run: (c) => c.logs.ingest({ resourceLogs: [] }, options),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "trace ingest",
|
|
89
|
+
method: "POST",
|
|
90
|
+
path: "/v1/traces",
|
|
91
|
+
body: { resourceSpans: [] },
|
|
92
|
+
run: (c) => c.traces.ingest({ resourceSpans: [] }, options),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "test rule",
|
|
96
|
+
method: "POST",
|
|
97
|
+
path: "/v1/alerts/a%2Fb/test",
|
|
98
|
+
body: {},
|
|
99
|
+
run: (c) => c.alerts.test("a/b", options),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "silence rule",
|
|
103
|
+
method: "POST",
|
|
104
|
+
path: "/v1/alerts/a%2Fb/silence",
|
|
105
|
+
body: { until: "2026-10-07T00:00:00Z" },
|
|
106
|
+
run: (c) => c.alerts.silence("a/b", { until: "2026-10-07T00:00:00Z" }, options),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
for (const product of ["logs", "traces", "alerts"]) {
|
|
110
|
+
const path = `/v1/${product}${product === "alerts" ? "" : "/views"}`;
|
|
111
|
+
const resource = (c) => (product === "alerts" ? c.alerts : c[product].views);
|
|
112
|
+
const input = product === "alerts" ? rule : view;
|
|
113
|
+
calls.push({
|
|
114
|
+
name: `${product} create`,
|
|
115
|
+
method: "POST",
|
|
116
|
+
path,
|
|
117
|
+
body: input,
|
|
118
|
+
run: (c) => resource(c).create(input, options),
|
|
119
|
+
}, {
|
|
120
|
+
name: `${product} get`,
|
|
121
|
+
method: "GET",
|
|
122
|
+
path: `${path}/a%2Fb`,
|
|
123
|
+
run: (c) => resource(c).get("a/b"),
|
|
124
|
+
}, {
|
|
125
|
+
name: `${product} list`,
|
|
126
|
+
method: "GET",
|
|
127
|
+
path,
|
|
128
|
+
query: { limit: "2", cursor: "next" },
|
|
129
|
+
run: (c) => resource(c).list({ limit: 2, cursor: "next" }),
|
|
130
|
+
}, {
|
|
131
|
+
name: `${product} update`,
|
|
132
|
+
method: "PUT",
|
|
133
|
+
path: `${path}/a%2Fb`,
|
|
134
|
+
body: { name: "changed" },
|
|
135
|
+
run: (c) => resource(c).update("a/b", { name: "changed" }, options),
|
|
136
|
+
}, {
|
|
137
|
+
name: `${product} delete`,
|
|
138
|
+
method: "DELETE",
|
|
139
|
+
path: `${path}/a%2Fb`,
|
|
140
|
+
run: (c) => resource(c).delete("a/b", options),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
for (const entry of calls)
|
|
144
|
+
describe(entry.name, () => {
|
|
145
|
+
test("request, encoded path and idempotency", async () => {
|
|
146
|
+
const { client, requests } = fixture(() => entry.method === "DELETE"
|
|
147
|
+
? new Response(null, { status: 204 })
|
|
148
|
+
: Response.json({ data: [], nextCursor: null, marker: "answer" }));
|
|
149
|
+
const answer = await entry.run(client);
|
|
150
|
+
if (entry.method === "DELETE")
|
|
151
|
+
expect(answer).toBeNull();
|
|
152
|
+
else if (!entry.name.endsWith(" list"))
|
|
153
|
+
expect(answer).toEqual({ data: [], nextCursor: null, marker: "answer" });
|
|
154
|
+
expect(requests).toHaveLength(1);
|
|
155
|
+
const request = requests[0];
|
|
156
|
+
expect(request.url.pathname).toBe(entry.path);
|
|
157
|
+
expect(request.init.method).toBe(entry.method);
|
|
158
|
+
expect(Object.fromEntries(request.url.searchParams)).toEqual(entry.query ?? {});
|
|
159
|
+
if (entry.body !== undefined)
|
|
160
|
+
expect(JSON.parse(request.init.body)).toEqual(entry.body);
|
|
161
|
+
if (entry.method !== "GET")
|
|
162
|
+
expect(new Headers(request.init.headers).get("idempotency-key")).toBe(options.idempotencyKey);
|
|
163
|
+
});
|
|
164
|
+
test("typed server error preserves code and hint", async () => {
|
|
165
|
+
const { client } = fixture(() => Response.json({ error: { code: "not_found", message: "Missing", hint: "List resources first." } }, { status: 404 }));
|
|
166
|
+
try {
|
|
167
|
+
await entry.run(client);
|
|
168
|
+
throw new Error("Accepted failure");
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
expect(error).toBeInstanceOf(NotFoundError);
|
|
172
|
+
expect(error).toMatchObject({ code: "not_found", hint: "List resources first." });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
test("search paging follows opaque cursors and preserves equal timestamp lines", async () => {
|
|
177
|
+
const { client, requests } = fixture((_url, init) => Response.json({
|
|
178
|
+
lines: [line],
|
|
179
|
+
next: JSON.parse(init.body).cursor ? null : "opaque/+",
|
|
180
|
+
stats: {},
|
|
181
|
+
}));
|
|
182
|
+
const pages = [];
|
|
183
|
+
for await (const page of client.logs.pages({ query: "{}", limit: 1 }))
|
|
184
|
+
pages.push(page);
|
|
185
|
+
expect(pages.flatMap((page) => page.lines)).toEqual([line, line]);
|
|
186
|
+
expect(JSON.parse(requests[1].init.body).cursor).toBe("opaque/+");
|
|
187
|
+
});
|
|
188
|
+
test("search detects a cursor loop", async () => {
|
|
189
|
+
const { client } = fixture(() => Response.json({ lines: [], next: "same" }));
|
|
190
|
+
const iterator = client.logs.pages({ query: "{}", cursor: "same" });
|
|
191
|
+
await iterator.next();
|
|
192
|
+
await expect(iterator.next()).rejects.toMatchObject({ code: "invalid_stream" });
|
|
193
|
+
});
|
|
194
|
+
test("resource lists can follow pages", async () => {
|
|
195
|
+
const { client } = fixture((url) => Response.json({
|
|
196
|
+
data: [{ id: url.searchParams.get("cursor") ?? "first" }],
|
|
197
|
+
nextCursor: url.searchParams.has("cursor") ? null : "second",
|
|
198
|
+
}));
|
|
199
|
+
expect(await (await client.logs.views.list()).toArray()).toEqual([
|
|
200
|
+
{ id: "first" },
|
|
201
|
+
{ id: "second" },
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
test("UTF-8 bytes enforce the 1 MiB ingest bound before fetch", () => {
|
|
205
|
+
const { client, requests } = fixture(() => {
|
|
206
|
+
throw new Error("No fetch allowed");
|
|
207
|
+
});
|
|
208
|
+
expect(() => client.logs.ingest({ resourceLogs: [{ body: "α".repeat(530_000) }] })).toThrow(InvalidRequestError);
|
|
209
|
+
expect(() => client.traces.ingest({ resourceSpans: [{ body: "α".repeat(530_000) }] })).toThrow(InvalidRequestError);
|
|
210
|
+
expect(requests).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
const event = (id, value = line, kind = "") => `id: ${id}\r\n${kind ? `event: ${kind}\r\n` : ""}data: ${JSON.stringify(value)}\r\n\r\n`;
|
|
213
|
+
function sse(text, bytes = 7) {
|
|
214
|
+
const encoded = new TextEncoder().encode(text);
|
|
215
|
+
let at = 0;
|
|
216
|
+
return new Response(new ReadableStream({
|
|
217
|
+
pull(controller) {
|
|
218
|
+
if (at === encoded.length)
|
|
219
|
+
controller.close();
|
|
220
|
+
else {
|
|
221
|
+
controller.enqueue(encoded.slice(at, at + bytes));
|
|
222
|
+
at = Math.min(at + bytes, encoded.length);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
}), { headers: { "content-type": "text/event-stream" } });
|
|
226
|
+
}
|
|
227
|
+
test("SSE reconnects by cursor, discards torn events, keeps identical records and decodes split UTF-8", async () => {
|
|
228
|
+
let count = 0;
|
|
229
|
+
const { client, requests } = fixture(() => ++count === 1
|
|
230
|
+
? sse(`: heartbeat\r\n\r\n${event("one")}id: torn\ndata: {`)
|
|
231
|
+
: sse(event("one") + event("two")));
|
|
232
|
+
const iterator = client.logs.tail("{}", { reconnectMs: 0 });
|
|
233
|
+
expect((await iterator.next()).value).toEqual({ ...line, cursor: "one" });
|
|
234
|
+
expect((await iterator.next()).value).toEqual({ ...line, cursor: "two" });
|
|
235
|
+
expect(requests[1].url.searchParams.get("cursor")).toBe("one");
|
|
236
|
+
expect(new Headers(requests[0].init.headers).get("accept")).toBe("text/event-stream");
|
|
237
|
+
await iterator.return(undefined);
|
|
238
|
+
});
|
|
239
|
+
test("a drop before the first line resumes from the start event's cursor", async () => {
|
|
240
|
+
let count = 0;
|
|
241
|
+
const { client, requests } = fixture(() => ++count === 1
|
|
242
|
+
? sse(event("began", { cursor: "began" }, "start"))
|
|
243
|
+
: sse(event("began", { cursor: "began" }, "start") + event("first")));
|
|
244
|
+
const iterator = client.logs.tail("{}", { reconnectMs: 0 });
|
|
245
|
+
expect((await iterator.next()).value).toEqual({ ...line, cursor: "first" });
|
|
246
|
+
expect(requests[0].url.searchParams.get("cursor")).toBeNull();
|
|
247
|
+
expect(requests[1].url.searchParams.get("cursor")).toBe("began");
|
|
248
|
+
await iterator.return(undefined);
|
|
249
|
+
});
|
|
250
|
+
test("server retention gap becomes typed from/to and resumes after it", async () => {
|
|
251
|
+
const { client } = fixture(() => sse(event("kept", { cursor: "kept" }, "gap") + event("later")));
|
|
252
|
+
const iterator = client.logs.tail("{}", { cursor: "expired", reconnectMs: 0 });
|
|
253
|
+
expect((await iterator.next()).value).toEqual({ gap: true, from: "expired", to: "kept" });
|
|
254
|
+
expect((await iterator.next()).value).toMatchObject({ cursor: "later" });
|
|
255
|
+
await iterator.return(undefined);
|
|
256
|
+
});
|
|
257
|
+
test("reader error, including an echoed cursor, reconnects without moving the cursor", async () => {
|
|
258
|
+
let calls = 0;
|
|
259
|
+
const { client, requests } = fixture(() => sse(++calls === 1 ? event("old", { message: "reader gone" }, "error") : event("new")));
|
|
260
|
+
const iterator = client.logs.tail("{}", { cursor: "old", reconnectMs: 0 });
|
|
261
|
+
expect((await iterator.next()).value).toMatchObject({ cursor: "new" });
|
|
262
|
+
expect(requests[1].url.searchParams.get("cursor")).toBe("old");
|
|
263
|
+
await iterator.return(undefined);
|
|
264
|
+
});
|
|
265
|
+
for (const [name, wire] of [
|
|
266
|
+
["JSON", "id: a\ndata: nope\n\n"],
|
|
267
|
+
["cursor", `data: ${JSON.stringify(line)}\n\n`],
|
|
268
|
+
["line", event("a", {})],
|
|
269
|
+
["gap", event("a", { cursor: "wrong" }, "gap")],
|
|
270
|
+
["oversized event", `id: a\ndata: ${"x".repeat(1_048_577)}\n\n`],
|
|
271
|
+
])
|
|
272
|
+
test(`invalid ${name} fails once with a code and hint`, async () => {
|
|
273
|
+
const { client, requests } = fixture(() => sse(wire, wire.length));
|
|
274
|
+
await expect(client.logs.tail("{}", { cursor: "before", reconnectMs: 0 }).next()).rejects.toMatchObject({ code: "invalid_stream", hint: expect.any(String) });
|
|
275
|
+
expect(requests).toHaveLength(1);
|
|
276
|
+
});
|
|
277
|
+
test("tail authentication errors do not reconnect", async () => {
|
|
278
|
+
const { client, requests } = fixture(() => Response.json({ error: { code: "unauthorized", hint: "Sign in." } }, { status: 401 }));
|
|
279
|
+
await expect(client.logs.tail("{}").next()).rejects.toMatchObject({
|
|
280
|
+
code: "unauthorized",
|
|
281
|
+
hint: "Sign in.",
|
|
282
|
+
});
|
|
283
|
+
expect(requests).toHaveLength(1);
|
|
284
|
+
});
|
|
285
|
+
test("aborting an idle connection closes its reader", async () => {
|
|
286
|
+
let cancelled = false;
|
|
287
|
+
const { client } = fixture(() => new Response(new ReadableStream({
|
|
288
|
+
cancel() {
|
|
289
|
+
cancelled = true;
|
|
290
|
+
},
|
|
291
|
+
}), { headers: { "content-type": "text/event-stream" } }));
|
|
292
|
+
const abort = new AbortController();
|
|
293
|
+
const iterator = client.logs.tail("{}", { signal: abort.signal });
|
|
294
|
+
const pending = iterator.next();
|
|
295
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
296
|
+
abort.abort(new Error("test stop"));
|
|
297
|
+
await expect(pending).rejects.toThrow("test stop");
|
|
298
|
+
expect(cancelled).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
test("non-SSE success is refused", async () => {
|
|
301
|
+
const { client } = fixture(() => Response.json({}));
|
|
302
|
+
await expect(client.logs.tail("{}").next()).rejects.toBeInstanceOf(RuntimeError);
|
|
303
|
+
});
|
|
304
|
+
test("an abrupt read failure reconnects, rather than only handling clean EOF", async () => {
|
|
305
|
+
let attempt = 0;
|
|
306
|
+
const { client, requests } = fixture(() => {
|
|
307
|
+
if (++attempt > 1)
|
|
308
|
+
return sse(event("second"));
|
|
309
|
+
let pulled = false;
|
|
310
|
+
return new Response(new ReadableStream({
|
|
311
|
+
pull(controller) {
|
|
312
|
+
if (!pulled) {
|
|
313
|
+
pulled = true;
|
|
314
|
+
controller.enqueue(new TextEncoder().encode(event("first")));
|
|
315
|
+
}
|
|
316
|
+
else
|
|
317
|
+
controller.error(new Error("connection reset"));
|
|
318
|
+
},
|
|
319
|
+
}), { headers: { "content-type": "text/event-stream" } });
|
|
320
|
+
});
|
|
321
|
+
const iterator = client.logs.tail("{}", { reconnectMs: 0 });
|
|
322
|
+
expect((await iterator.next()).value).toMatchObject({ cursor: "first" });
|
|
323
|
+
expect((await iterator.next()).value).toMatchObject({ cursor: "second" });
|
|
324
|
+
expect(requests[1].url.searchParams.get("cursor")).toBe("first");
|
|
325
|
+
await iterator.return(undefined);
|
|
326
|
+
});
|
|
327
|
+
test("a multline SSE event and the agreed from/to gap shape are accepted", async () => {
|
|
328
|
+
const { client } = fixture(() => sse('event: gap\nid: kept\ndata: {"from":"old",\ndata: "to":"kept"}\n\n'));
|
|
329
|
+
const iterator = client.logs.tail("{}", { cursor: "old" });
|
|
330
|
+
expect((await iterator.next()).value).toEqual({ gap: true, from: "old", to: "kept" });
|
|
331
|
+
await iterator.return(undefined);
|
|
332
|
+
});
|
|
333
|
+
test("abort during reconnect delay stops without another request", async () => {
|
|
334
|
+
const abort = new AbortController();
|
|
335
|
+
const { client, requests } = fixture(() => sse(event("first")));
|
|
336
|
+
const iterator = client.logs.tail("{}", { signal: abort.signal, reconnectMs: 1000 });
|
|
337
|
+
await iterator.next();
|
|
338
|
+
const pending = iterator.next();
|
|
339
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
340
|
+
abort.abort(new Error("stop reconnect"));
|
|
341
|
+
await expect(pending).rejects.toThrow("stop reconnect");
|
|
342
|
+
expect(requests).toHaveLength(1);
|
|
343
|
+
});
|
|
@@ -129,9 +129,9 @@ export declare function ports(t: Transport): {
|
|
|
129
129
|
deleted: true;
|
|
130
130
|
}>;
|
|
131
131
|
};
|
|
132
|
-
/** `runtime.
|
|
132
|
+
/** `runtime.ips`: a dedicated outbound address every sandbox of the
|
|
133
133
|
* account sends from, for allow-lists. */
|
|
134
|
-
export declare function
|
|
134
|
+
export declare function ips(t: Transport): {
|
|
135
135
|
reserve: (input?: {
|
|
136
136
|
family?: 4 | 6;
|
|
137
137
|
}, options?: RequestOptions) => Promise<EgressAddress>;
|
|
@@ -142,8 +142,8 @@ export declare function addresses(t: Transport): {
|
|
|
142
142
|
deleted: true;
|
|
143
143
|
}>;
|
|
144
144
|
};
|
|
145
|
-
/** `runtime.
|
|
146
|
-
export declare function
|
|
145
|
+
/** `runtime.networks.wireguard`: a WireGuard tunnel from your own network into your sandboxes. */
|
|
146
|
+
export declare function wireguard(t: Transport): {
|
|
147
147
|
get: (options?: RequestOptions) => Promise<Tunnel>;
|
|
148
148
|
/** Idempotent: returns the tunnel you have. */
|
|
149
149
|
create: (input?: {
|
|
@@ -186,15 +186,39 @@ export type SetUpstreamProxy = {
|
|
|
186
186
|
* every connection when absent. 1 to 16. */
|
|
187
187
|
hosts?: string[];
|
|
188
188
|
};
|
|
189
|
-
/** `runtime.
|
|
189
|
+
/** `runtime.networks`: the networks of the whole account: the WireGuard
|
|
190
|
+
* tunnel in, your sandboxes reaching each other, and the proxy out.
|
|
190
191
|
*
|
|
191
|
-
* await runtime.
|
|
192
|
+
* await runtime.networks.upstreamProxy.set({ url: "http://proxy.example.com:3128" });
|
|
192
193
|
*
|
|
193
194
|
* `upstreamProxy`: after Runtime's own rules allow a connection, the host
|
|
194
195
|
* reaches it with CONNECT through your proxy, so your proxy's controls and
|
|
195
196
|
* logs apply. A proxy that refuses or cannot be reached fails the connection
|
|
196
197
|
* (upstream-proxy-failed); nothing goes around it. Needs a kept top-up. */
|
|
197
|
-
export declare function
|
|
198
|
+
export declare function networks(t: Transport): {
|
|
199
|
+
/** The WireGuard tunnel from your own network into your sandboxes. */
|
|
200
|
+
wireguard: {
|
|
201
|
+
get: (options?: RequestOptions) => Promise<Tunnel>;
|
|
202
|
+
/** Idempotent: returns the tunnel you have. */
|
|
203
|
+
create: (input?: {
|
|
204
|
+
subnet?: string;
|
|
205
|
+
}, options?: RequestOptions) => Promise<Tunnel>;
|
|
206
|
+
delete: (options?: RequestOptions) => Promise<{
|
|
207
|
+
id: string;
|
|
208
|
+
deleted: true;
|
|
209
|
+
}>;
|
|
210
|
+
/** Leave publicKey out and Runtime generates the pair; the private key is
|
|
211
|
+
* then in `config`, shown once. */
|
|
212
|
+
addPeer: (input: {
|
|
213
|
+
name: string;
|
|
214
|
+
publicKey?: string;
|
|
215
|
+
routes?: string[];
|
|
216
|
+
}, options?: RequestOptions) => Promise<TunnelPeerCreated>;
|
|
217
|
+
rotatePeer: (peerId: string, input?: {
|
|
218
|
+
publicKey?: string;
|
|
219
|
+
}, options?: RequestOptions) => Promise<TunnelPeerCreated>;
|
|
220
|
+
removePeer: (peerId: string, options?: RequestOptions) => Promise<Tunnel>;
|
|
221
|
+
};
|
|
198
222
|
/** Your sandboxes reaching each other by name (`private-network.ts`). */
|
|
199
223
|
private: {
|
|
200
224
|
get: (options?: RequestOptions) => Promise<import("./private-network.js").PrivateNetwork>;
|
|
@@ -202,6 +226,13 @@ export declare function network(t: Transport): {
|
|
|
202
226
|
enabled: boolean;
|
|
203
227
|
}, options?: RequestOptions) => Promise<import("./private-network.js").PrivateNetwork>;
|
|
204
228
|
};
|
|
229
|
+
/** Your sandboxes' requests signed as Runtime's agents (`signed-requests.ts`). */
|
|
230
|
+
signedRequests: {
|
|
231
|
+
get: (options?: RequestOptions) => Promise<import("./signed-requests.js").SignedRequests>;
|
|
232
|
+
set: (input: {
|
|
233
|
+
enabled: boolean;
|
|
234
|
+
}, options?: RequestOptions) => Promise<import("./signed-requests.js").SignedRequests>;
|
|
235
|
+
};
|
|
205
236
|
upstreamProxy: {
|
|
206
237
|
/** Send outbound connections through your proxy, replacing any set before. */
|
|
207
238
|
set: (input: SetUpstreamProxy, options?: RequestOptions) => Promise<UpstreamProxy & {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { privateNetwork } from "./private-network.js";
|
|
2
|
+
import { signedRequests } from "./signed-requests.js";
|
|
2
3
|
const enc = encodeURIComponent;
|
|
3
4
|
/** `runtime.domains`: serve a sandbox's port at your own hostname, with HTTPS. */
|
|
4
5
|
export function domains(t) {
|
|
@@ -43,57 +44,69 @@ export function ports(t) {
|
|
|
43
44
|
}),
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
|
-
/** `runtime.
|
|
47
|
+
/** `runtime.ips`: a dedicated outbound address every sandbox of the
|
|
47
48
|
* account sends from, for allow-lists. */
|
|
48
|
-
export function
|
|
49
|
+
export function ips(t) {
|
|
49
50
|
return {
|
|
50
|
-
reserve: (input = {}, options) => t.json({ method: "POST", path: "/v1/
|
|
51
|
-
list: async (options) => (await t.json({ method: "GET", path: "/v1/
|
|
52
|
-
.data,
|
|
51
|
+
reserve: (input = {}, options) => t.json({ method: "POST", path: "/v1/ips", body: input, ...options }),
|
|
52
|
+
list: async (options) => (await t.json({ method: "GET", path: "/v1/ips", ...options })).data,
|
|
53
53
|
release: (addressId, options) => t.json({
|
|
54
54
|
method: "DELETE",
|
|
55
|
-
path: `/v1/
|
|
55
|
+
path: `/v1/ips/${enc(addressId)}`,
|
|
56
56
|
...options,
|
|
57
57
|
}),
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
-
/** `runtime.
|
|
61
|
-
export function
|
|
60
|
+
/** `runtime.networks.wireguard`: a WireGuard tunnel from your own network into your sandboxes. */
|
|
61
|
+
export function wireguard(t) {
|
|
62
62
|
return {
|
|
63
|
-
get: (options) => t.json({ method: "GET", path: "/v1/
|
|
63
|
+
get: (options) => t.json({ method: "GET", path: "/v1/networks/wireguard", ...options }),
|
|
64
64
|
/** Idempotent: returns the tunnel you have. */
|
|
65
|
-
create: (input = {}, options) => t.json({ method: "POST", path: "/v1/
|
|
66
|
-
delete: (options) => t.json({
|
|
65
|
+
create: (input = {}, options) => t.json({ method: "POST", path: "/v1/networks/wireguard", body: input, ...options }),
|
|
66
|
+
delete: (options) => t.json({
|
|
67
|
+
method: "DELETE",
|
|
68
|
+
path: "/v1/networks/wireguard",
|
|
69
|
+
...options,
|
|
70
|
+
}),
|
|
67
71
|
/** Leave publicKey out and Runtime generates the pair; the private key is
|
|
68
72
|
* then in `config`, shown once. */
|
|
69
73
|
addPeer: (input, options) => t.json({
|
|
70
74
|
method: "POST",
|
|
71
|
-
path: "/v1/
|
|
75
|
+
path: "/v1/networks/wireguard/peers",
|
|
72
76
|
body: input,
|
|
73
77
|
...options,
|
|
74
78
|
}),
|
|
75
79
|
rotatePeer: (peerId, input = {}, options) => t.json({
|
|
76
80
|
method: "POST",
|
|
77
|
-
path: `/v1/
|
|
81
|
+
path: `/v1/networks/wireguard/peers/${enc(peerId)}:rotate`,
|
|
78
82
|
body: input,
|
|
79
83
|
...options,
|
|
80
84
|
}),
|
|
81
|
-
removePeer: (peerId, options) => t.json({
|
|
85
|
+
removePeer: (peerId, options) => t.json({
|
|
86
|
+
method: "DELETE",
|
|
87
|
+
path: `/v1/networks/wireguard/peers/${enc(peerId)}`,
|
|
88
|
+
...options,
|
|
89
|
+
}),
|
|
82
90
|
};
|
|
83
91
|
}
|
|
84
|
-
/** `runtime.
|
|
92
|
+
/** `runtime.networks`: the networks of the whole account: the WireGuard
|
|
93
|
+
* tunnel in, your sandboxes reaching each other, and the proxy out.
|
|
85
94
|
*
|
|
86
|
-
* await runtime.
|
|
95
|
+
* await runtime.networks.upstreamProxy.set({ url: "http://proxy.example.com:3128" });
|
|
87
96
|
*
|
|
88
97
|
* `upstreamProxy`: after Runtime's own rules allow a connection, the host
|
|
89
98
|
* reaches it with CONNECT through your proxy, so your proxy's controls and
|
|
90
99
|
* logs apply. A proxy that refuses or cannot be reached fails the connection
|
|
91
100
|
* (upstream-proxy-failed); nothing goes around it. Needs a kept top-up. */
|
|
92
|
-
export function
|
|
93
|
-
const path = "/v1/
|
|
101
|
+
export function networks(t) {
|
|
102
|
+
const path = "/v1/networks/upstream-proxy";
|
|
94
103
|
return {
|
|
104
|
+
/** The WireGuard tunnel from your own network into your sandboxes. */
|
|
105
|
+
wireguard: wireguard(t),
|
|
95
106
|
/** Your sandboxes reaching each other by name (`private-network.ts`). */
|
|
96
107
|
private: privateNetwork(t),
|
|
108
|
+
/** Your sandboxes' requests signed as Runtime's agents (`signed-requests.ts`). */
|
|
109
|
+
signedRequests: signedRequests(t),
|
|
97
110
|
upstreamProxy: {
|
|
98
111
|
/** Send outbound connections through your proxy, replacing any set before. */
|
|
99
112
|
set: (input, options) => t.json({
|
|
@@ -35,7 +35,7 @@ export type SandboxMetrics = {
|
|
|
35
35
|
latest: MetricPoint | null;
|
|
36
36
|
points: MetricPoint[];
|
|
37
37
|
};
|
|
38
|
-
export type WebhookEventType = "sandbox.created" | "sandbox.running" | "sandbox.paused" | "sandbox.woken" | "sandbox.stopped" | "sandbox.start_failed" | "sandbox.wake_failed" | "sandbox.host_unreachable" | "sandbox.host_reachable" | "sandbox.recovered" | "sandbox.recovery_failed" | "snapshot.ready" | "snapshot.failed" | "snapshot.deleted" | "volume.ready" | "volume.failed" | "volume.deleted";
|
|
38
|
+
export type WebhookEventType = "sandbox.created" | "sandbox.running" | "sandbox.paused" | "sandbox.woken" | "sandbox.stopped" | "sandbox.start_failed" | "sandbox.wake_failed" | "sandbox.host_unreachable" | "sandbox.host_reachable" | "sandbox.recovered" | "sandbox.recovery_failed" | "sandbox.files.changed" | "sandbox.out_of_memory" | "sandbox.secret_not_applied" | "snapshot.ready" | "snapshot.failed" | "snapshot.deleted" | "volume.ready" | "volume.failed" | "volume.deleted" | "egress_secret.created" | "egress_secret.updated" | "egress_secret.deleted" | "upstream_proxy.set" | "upstream_proxy.removed";
|
|
39
39
|
/** An event as /v1/events lists it and a webhook delivers it. */
|
|
40
40
|
export type RuntimeEvent = {
|
|
41
41
|
id: string;
|
|
@@ -113,7 +113,8 @@ export type UsageExportRow = {
|
|
|
113
113
|
name: string | null;
|
|
114
114
|
kind: string;
|
|
115
115
|
meter: string;
|
|
116
|
-
|
|
116
|
+
/** credit, the trial's hours, or the month's included usage. */
|
|
117
|
+
fundedBy: "credit" | "trial" | "allowance";
|
|
117
118
|
startsAt: string;
|
|
118
119
|
endsAt: string;
|
|
119
120
|
settledAt: string;
|