withruntime 0.12.0 → 0.14.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/README.md +3 -3
- package/README.src.md +2 -2
- package/dist/billing-cli.js +8 -4
- package/dist/blaxel/sandbox.d.ts +1 -1
- package/dist/blaxel/sandbox.js +1 -1
- package/dist/cli.d.ts +17 -2
- package/dist/cli.js +242 -69
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +15 -0
- package/dist/daytona/daytona.d.ts +1 -1
- package/dist/daytona/daytona.js +1 -1
- package/dist/e2b/sandbox.d.ts +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/machine.d.ts +15 -1
- package/dist/machine.js +26 -11
- package/dist/mcp.js +144 -20
- 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/observability.d.ts +7 -2
- 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/transport.d.ts +4 -1
- package/dist/transport.js +25 -4
- package/dist/types.d.ts +9 -5
- package/dist/vercel/sandbox.d.ts +1 -1
- package/dist/vercel/sandbox.js +1 -1
- package/dist/wallet.d.ts +4 -1
- package/dist/wallet.js +2 -1
- package/package.json +2 -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/transport.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
|
|
2
2
|
import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
|
|
3
3
|
import { warningText } from "./product-renames.js";
|
|
4
|
-
export const VERSION = "0.
|
|
4
|
+
export const VERSION = "0.14.0";
|
|
5
5
|
export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
6
6
|
/** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
|
|
7
7
|
* own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
|
|
@@ -9,6 +9,9 @@ export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
|
9
9
|
* because the hop never leaves the machine: it goes from the program to the
|
|
10
10
|
* guest's own proxy and over the sandbox's private channel to its host. */
|
|
11
11
|
export const SANDBOX_BASE_URL = "http://runtime.internal";
|
|
12
|
+
/** How a Runtime secret's placeholder starts, the value a sandbox sees in its
|
|
13
|
+
* place (packages/cloud-guide/docs/security.md). */
|
|
14
|
+
export const PLACEHOLDER = "rtsec_";
|
|
12
15
|
/** Every Runtime sandbox has this file; guest-net.py keeps it current. Tests
|
|
13
16
|
* move it, so the machine running them does not decide what they see. */
|
|
14
17
|
export const sandboxMarker = { path: "/run/runtime/environment.json" };
|
|
@@ -204,7 +207,13 @@ export class Transport {
|
|
|
204
207
|
throw missingKey();
|
|
205
208
|
else
|
|
206
209
|
this.#apiKey = options.apiKey;
|
|
207
|
-
|
|
210
|
+
// A secret's placeholder becomes the key only on its way out through the
|
|
211
|
+
// sandbox's proxy; runtime.internal goes to the host without it, so the
|
|
212
|
+
// API sees the placeholder and answers 401 (land-lander, 11 October 2026:
|
|
213
|
+
// every worker's `check --remote`). Such a key's calls take the public API.
|
|
214
|
+
const placeholder = this.#apiKey?.startsWith(PLACEHOLDER) === true;
|
|
215
|
+
const origin = options.baseUrl || processEnv().RUNTIME_API_URL || DEFAULT_BASE_URL;
|
|
216
|
+
this.baseUrl = apiOrigin(placeholder ? origin : reachable(origin));
|
|
208
217
|
// The environment's proxy (HTTPS_PROXY, NO_PROXY) on Node and Bun alike;
|
|
209
218
|
// a fetch passed in is used as it is.
|
|
210
219
|
this.#fetch = options.fetch ?? envFetch;
|
|
@@ -301,6 +310,18 @@ export class Transport {
|
|
|
301
310
|
await sleep(ms, signal);
|
|
302
311
|
await gate?.take();
|
|
303
312
|
}
|
|
313
|
+
/** Wait before sending a refused call again. When the call's deadline
|
|
314
|
+
* ends the wait, the refusal stands: on a loaded machine bun can fire
|
|
315
|
+
* AbortSignal.timeout before a shorter setTimeout, and the call then
|
|
316
|
+
* failed as a timeout although the transport had judged the wait to fit
|
|
317
|
+
* (11 October 2026). A caller's own cancel still ends it as a cancel. */
|
|
318
|
+
async #retryAfter(refusal, ms, signal, gate) {
|
|
319
|
+
await this.#rest(ms, signal, gate).catch((cause) => {
|
|
320
|
+
throw signal?.reason?.name === "TimeoutError"
|
|
321
|
+
? refusal
|
|
322
|
+
: cause;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
304
325
|
async #send(call, consume, gate) {
|
|
305
326
|
let apiKey;
|
|
306
327
|
try {
|
|
@@ -413,7 +434,7 @@ export class Transport {
|
|
|
413
434
|
inCallback = true;
|
|
414
435
|
call.onCapacityWait?.(error, pause);
|
|
415
436
|
inCallback = false;
|
|
416
|
-
await this.#
|
|
437
|
+
await this.#retryAfter(error, pause, signal, gate);
|
|
417
438
|
continue;
|
|
418
439
|
}
|
|
419
440
|
// Nothing ran: safe to send again for any call, past maxRetries,
|
|
@@ -429,7 +450,7 @@ export class Transport {
|
|
|
429
450
|
const pause = Math.min(30_000, base * 2 ** Math.min(idleAttempt++, 3)) * (0.9 + Math.random() * 0.2);
|
|
430
451
|
if (pause >= idleUntil - performance.now())
|
|
431
452
|
throw error;
|
|
432
|
-
await this.#
|
|
453
|
+
await this.#retryAfter(error, pause, signal, gate);
|
|
433
454
|
continue;
|
|
434
455
|
}
|
|
435
456
|
const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
|
package/dist/types.d.ts
CHANGED
|
@@ -92,7 +92,7 @@ export type CreateSandbox = {
|
|
|
92
92
|
* `env`. At most 32 variables and 32 KiB. Values are never shown again;
|
|
93
93
|
* answers list only `envNames`. Copies made by fork() keep them. */
|
|
94
94
|
env?: Record<string, string>;
|
|
95
|
-
/** Omit to use the
|
|
95
|
+
/** Omit to use the free usage while it lasts, then prepaid credit. */
|
|
96
96
|
funding?: "trial" | "paid";
|
|
97
97
|
region?: string;
|
|
98
98
|
vcpu?: number;
|
|
@@ -295,11 +295,11 @@ export type Usage = {
|
|
|
295
295
|
/** @deprecated Read `availableMs`. Still sent for older clients. */
|
|
296
296
|
reservedMs: number;
|
|
297
297
|
availableMs: number;
|
|
298
|
-
/** `monthly`: the
|
|
299
|
-
* given before 5 October 2026, kept, beside the
|
|
298
|
+
/** `monthly`: the free usage every month only. `hours`: free hours
|
|
299
|
+
* given before 5 October 2026, kept, beside the free usage. Absent
|
|
300
300
|
* from an API older than 5 October 2026. */
|
|
301
301
|
offer?: "monthly" | "hours";
|
|
302
|
-
/** When the
|
|
302
|
+
/** When the free usage renews: the 1st of next month, 00:00 UTC. */
|
|
303
303
|
renewsAt?: string;
|
|
304
304
|
} | null;
|
|
305
305
|
/** What the account uses free this calendar month (UTC), one row per
|
|
@@ -318,9 +318,13 @@ export type Usage = {
|
|
|
318
318
|
/** The month's first day, YYYY-MM-DD. */
|
|
319
319
|
month: string;
|
|
320
320
|
renewsAt: string;
|
|
321
|
+
/** When the free machine time ends for an account that has never paid
|
|
322
|
+
* (30 days after it opened); null once it has paid, when it renews
|
|
323
|
+
* every month. Absent from an API older than 14 October 2026. */
|
|
324
|
+
freeUntil?: string | null;
|
|
321
325
|
}>;
|
|
322
326
|
/** What Runtime for Startups includes (https://withruntime.com/startups).
|
|
323
|
-
* Startup hours are given once, used after this month's
|
|
327
|
+
* Startup hours are given once, used after this month's free usage
|
|
324
328
|
* and before credit, never renewed, and gone after 12 months. Storage is
|
|
325
329
|
* GB kept free each month for those 12 months. Empty unless the account
|
|
326
330
|
* was accepted to the program or given it; absent from an older API. */
|
package/dist/vercel/sandbox.d.ts
CHANGED
|
@@ -147,7 +147,7 @@ export declare class Sandbox {
|
|
|
147
147
|
/** Creates a sandbox and waits until it runs. Vercel's defaults: 2 vCPUs,
|
|
148
148
|
* 2048 MiB per vCPU, persistent. With no `timeout` it has no time limit: it
|
|
149
149
|
* runs while it works and pauses when idle. A `timeout` is kept as given. Funding is left to
|
|
150
|
-
* Runtime: the
|
|
150
|
+
* Runtime: the free usage first, then prepaid credit, exactly as withruntime's own create. */
|
|
151
151
|
static create(params?: CreateSandboxParams): Promise<Sandbox & AsyncDisposable>;
|
|
152
152
|
/** A sandbox by name (or Runtime id), woken if it is paused unless
|
|
153
153
|
* `resume: false`. */
|
package/dist/vercel/sandbox.js
CHANGED
|
@@ -260,7 +260,7 @@ export class Sandbox {
|
|
|
260
260
|
/** Creates a sandbox and waits until it runs. Vercel's defaults: 2 vCPUs,
|
|
261
261
|
* 2048 MiB per vCPU, persistent. With no `timeout` it has no time limit: it
|
|
262
262
|
* runs while it works and pauses when idle. A `timeout` is kept as given. Funding is left to
|
|
263
|
-
* Runtime: the
|
|
263
|
+
* Runtime: the free usage first, then prepaid credit, exactly as withruntime's own create. */
|
|
264
264
|
static async create(params = {}) {
|
|
265
265
|
refuseCreate(params);
|
|
266
266
|
const client = clientFor(params);
|
package/dist/wallet.d.ts
CHANGED
|
@@ -17,8 +17,11 @@ export type WalletClaim = {
|
|
|
17
17
|
status: Exclude<Topup["status"], "paid">;
|
|
18
18
|
orgId: string;
|
|
19
19
|
topup: Topup | null;
|
|
20
|
+
/** What to do now, such as sending the top-up again when it has no address. */
|
|
21
|
+
next?: string;
|
|
20
22
|
};
|
|
21
|
-
/** Opens an account and a stablecoin top-up of `usd` dollars ($10 to $
|
|
23
|
+
/** Opens an account and a stablecoin top-up of `usd` dollars ($10 to $500,
|
|
24
|
+
* a new account's first week).
|
|
22
25
|
* `acceptTerms` must be true: opening one means agreeing to
|
|
23
26
|
* https://withruntime.com/legal/terms, so ask the person you work for first. */
|
|
24
27
|
export declare function openWalletAccount(input: {
|
package/dist/wallet.js
CHANGED
|
@@ -33,7 +33,8 @@ async function post(options, path, body) {
|
|
|
33
33
|
});
|
|
34
34
|
return [response.status, answer];
|
|
35
35
|
}
|
|
36
|
-
/** Opens an account and a stablecoin top-up of `usd` dollars ($10 to $
|
|
36
|
+
/** Opens an account and a stablecoin top-up of `usd` dollars ($10 to $500,
|
|
37
|
+
* a new account's first week).
|
|
37
38
|
* `acceptTerms` must be true: opening one means agreeing to
|
|
38
39
|
* https://withruntime.com/legal/terms, so ask the person you work for first. */
|
|
39
40
|
export async function openWalletAccount(input, options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "withruntime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"mcpName": "com.withruntime/runtime",
|
|
6
6
|
"type": "module",
|
|
@@ -161,19 +161,9 @@
|
|
|
161
161
|
},
|
|
162
162
|
"scripts": {
|
|
163
163
|
"build": "tsc -p tsconfig.build.json",
|
|
164
|
-
"prepack": "bun run build",
|
|
165
164
|
"check-types": "tsc --noEmit",
|
|
166
165
|
"lint": "eslint",
|
|
167
|
-
"test": "bun
|
|
168
|
-
"test:java": "bun ../../sdks/java/scripts/check.ts",
|
|
169
|
-
"test:ruby": "bun ../../sdks/ruby/scripts/check.ts",
|
|
170
|
-
"test:python": "bun scripts/test-python.ts",
|
|
171
|
-
"test:go": "bun scripts/test-go.ts",
|
|
172
|
-
"compat:upstream": "bun scripts/check-upstream.ts",
|
|
173
|
-
"test:compat": "bun test tests/e2b tests/daytona tests/vercel tests/blaxel tests/compat tests/drop-in-isolation.test.ts tests/drop-in-paths.test.ts tests/process-input.test.ts tests/watch-reentrant.test.ts tests/upstream-check.test.ts tests/process-output.test.ts tests/prepare-compatibility.test.ts",
|
|
174
|
-
"compat:prepare": "bun scripts/prepare-compatibility.ts",
|
|
175
|
-
"compat:inventory": "bun scripts/compat-inventory.ts",
|
|
176
|
-
"compat:oracle": "bun oracle/check.mjs"
|
|
166
|
+
"test": "bun test"
|
|
177
167
|
},
|
|
178
168
|
"devDependencies": {
|
|
179
169
|
"@ai-sdk/harness": "1.0.124",
|
|
@@ -187,8 +177,6 @@
|
|
|
187
177
|
"@openai/agents": "0.18.0",
|
|
188
178
|
"@openai/agents-core": "0.18.0",
|
|
189
179
|
"@openrouter/sdk": "1.3.27",
|
|
190
|
-
"@runtime/cloud": "workspace:*",
|
|
191
|
-
"@runtime/cloud-guide": "workspace:*",
|
|
192
180
|
"@runtime/eslint-config": "workspace:*",
|
|
193
181
|
"@runtime/typescript-config": "workspace:*",
|
|
194
182
|
"@types/bun": "^1.4.0",
|
package/dist/cli/logs.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/cli/logs.test.js
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { logsProductCommand } from "./logs.js";
|
|
3
|
-
import { terminalSafe } from "../terminal.js";
|
|
4
|
-
import { logs, traces, alerts } from "../products/logs.js";
|
|
5
|
-
import { Transport } from "../transport.js";
|
|
6
|
-
import { InvalidRequestError } from "../errors.js";
|
|
7
|
-
const input = {
|
|
8
|
-
name: "errors",
|
|
9
|
-
labels: {},
|
|
10
|
-
query: '{app="api"}',
|
|
11
|
-
condition: "above 5",
|
|
12
|
-
for: "5m",
|
|
13
|
-
actions: [],
|
|
14
|
-
};
|
|
15
|
-
const cases = [
|
|
16
|
-
["logs", ["query", "{}"], { from: "1", to: "2", limit: "5" }, "GET", "/v1/logs/query_range"],
|
|
17
|
-
["logs", ["labels"], {}, "GET", "/v1/logs/labels"],
|
|
18
|
-
["logs", ["values", "app"], {}, "GET", "/v1/logs/labels/app/values"],
|
|
19
|
-
["logs", ["ingest"], { body: '{"resourceLogs":[]}' }, "POST", "/v1/logs"],
|
|
20
|
-
["traces", ["ingest"], { body: '{"resourceSpans":[]}' }, "POST", "/v1/traces"],
|
|
21
|
-
["traces", ["get", "id"], {}, "GET", "/v1/traces/id"],
|
|
22
|
-
["traces", ["search", "{}"], {}, "GET", "/v1/traces:search"],
|
|
23
|
-
["traces", ["tags"], {}, "GET", "/v1/traces/tags"],
|
|
24
|
-
["alerts", ["test", "id"], {}, "POST", "/v1/alerts/id/test"],
|
|
25
|
-
[
|
|
26
|
-
"alerts",
|
|
27
|
-
["silence", "id"],
|
|
28
|
-
{ until: "2026-10-07T00:00:00Z", reason: "maintenance" },
|
|
29
|
-
"POST",
|
|
30
|
-
"/v1/alerts/id/silence",
|
|
31
|
-
],
|
|
32
|
-
];
|
|
33
|
-
for (const product of ["logs", "traces", "alerts"])
|
|
34
|
-
for (const verb of ["ls", "get", "create", "update", "delete"]) {
|
|
35
|
-
const collection = `/v1/${product}${product === "alerts" ? "" : "/views"}`;
|
|
36
|
-
cases.push([
|
|
37
|
-
product,
|
|
38
|
-
[
|
|
39
|
-
...(product === "alerts" ? [] : ["views"]),
|
|
40
|
-
verb,
|
|
41
|
-
...(["get", "update", "delete"].includes(verb) ? ["id"] : []),
|
|
42
|
-
],
|
|
43
|
-
["create", "update"].includes(verb) ? { body: JSON.stringify(input) } : {},
|
|
44
|
-
{ ls: "GET", get: "GET", create: "POST", update: "PUT", delete: "DELETE" }[verb],
|
|
45
|
-
collection + (["get", "update", "delete"].includes(verb) ? "/id" : ""),
|
|
46
|
-
]);
|
|
47
|
-
}
|
|
48
|
-
function setup() {
|
|
49
|
-
const requests = [];
|
|
50
|
-
const transport = new Transport({
|
|
51
|
-
apiKey: "fake",
|
|
52
|
-
baseUrl: "https://fake.test",
|
|
53
|
-
maxRetries: 0,
|
|
54
|
-
fetch: (async (url, init) => {
|
|
55
|
-
requests.push({ url: new URL(url instanceof Request ? url.url : url), init: init ?? {} });
|
|
56
|
-
return Response.json({ ...input, id: "id", data: [], nextCursor: null, marker: "ok" });
|
|
57
|
-
}),
|
|
58
|
-
});
|
|
59
|
-
return {
|
|
60
|
-
client: { logs: logs(transport), traces: traces(transport), alerts: alerts(transport) },
|
|
61
|
-
requests,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
for (const [product, positional, flags, method, path] of cases)
|
|
65
|
-
for (const mode of ["tty", "pipe", "json"])
|
|
66
|
-
test(`${product} ${positional.join(" ")} ${mode}`, async () => {
|
|
67
|
-
const { client, requests } = setup(), output = [];
|
|
68
|
-
const result = await logsProductCommand(product, positional, {
|
|
69
|
-
positional,
|
|
70
|
-
flags: new Map(Object.entries({ ...flags, "idempotency-key": "retry-key" }).map(([key, value]) => [
|
|
71
|
-
key,
|
|
72
|
-
[value],
|
|
73
|
-
])),
|
|
74
|
-
}, {
|
|
75
|
-
json: mode === "json",
|
|
76
|
-
tty: mode === "tty",
|
|
77
|
-
write: (text) => output.push(text),
|
|
78
|
-
error: () => {
|
|
79
|
-
throw new Error("unexpected stderr");
|
|
80
|
-
},
|
|
81
|
-
}, async () => client);
|
|
82
|
-
expect(result).toBe(0);
|
|
83
|
-
expect(requests).toHaveLength(1);
|
|
84
|
-
expect(requests[0].url.pathname).toBe(path);
|
|
85
|
-
expect(requests[0].init.method).toBe(method);
|
|
86
|
-
expect(output).toHaveLength(1);
|
|
87
|
-
expect(output[0]).not.toContain("\x1b");
|
|
88
|
-
const printed = JSON.parse(output[0]);
|
|
89
|
-
if (positional.includes("delete"))
|
|
90
|
-
expect(printed).toEqual({ deleted: "id" });
|
|
91
|
-
else if (positional.includes("ls"))
|
|
92
|
-
expect(printed).toEqual({ data: [], nextCursor: null });
|
|
93
|
-
else
|
|
94
|
-
expect(printed.marker).toBe("ok");
|
|
95
|
-
if (method !== "GET")
|
|
96
|
-
expect(new Headers(requests[0].init.headers).get("idempotency-key")).toBe("retry-key");
|
|
97
|
-
if (positional[0] === "query")
|
|
98
|
-
expect(Object.fromEntries(requests[0].url.searchParams)).toEqual({
|
|
99
|
-
query: "{}",
|
|
100
|
-
start: "1",
|
|
101
|
-
end: "2",
|
|
102
|
-
limit: "5",
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
for (const mode of ["tty", "pipe", "json"])
|
|
106
|
-
test(`tail ${mode}, selector escaping, gap and backpressure`, async () => {
|
|
107
|
-
const { client } = setup();
|
|
108
|
-
const output = [];
|
|
109
|
-
let drained = 0;
|
|
110
|
-
const events = [
|
|
111
|
-
{
|
|
112
|
-
time: "12:00",
|
|
113
|
-
timeUnixNano: "1",
|
|
114
|
-
severity: 9,
|
|
115
|
-
severityText: "INFO",
|
|
116
|
-
labels: {},
|
|
117
|
-
attributes: {},
|
|
118
|
-
traceId: null,
|
|
119
|
-
spanId: null,
|
|
120
|
-
body: "line\x1b[2J\nnext",
|
|
121
|
-
cursor: "one",
|
|
122
|
-
},
|
|
123
|
-
{ gap: true, from: "old", to: "kept" },
|
|
124
|
-
];
|
|
125
|
-
let received;
|
|
126
|
-
client.logs.tail = async function* (query, options) {
|
|
127
|
-
received = { query, options };
|
|
128
|
-
yield* events;
|
|
129
|
-
};
|
|
130
|
-
const positional = ["tail", 'app"name'];
|
|
131
|
-
await logsProductCommand("logs", positional, {
|
|
132
|
-
positional,
|
|
133
|
-
flags: new Map([
|
|
134
|
-
["since", ["1h"]],
|
|
135
|
-
["grep", ['a"b']],
|
|
136
|
-
]),
|
|
137
|
-
}, {
|
|
138
|
-
json: mode === "json",
|
|
139
|
-
tty: mode === "tty",
|
|
140
|
-
write: (text) => output.push(text),
|
|
141
|
-
error() { },
|
|
142
|
-
async drain() {
|
|
143
|
-
drained++;
|
|
144
|
-
},
|
|
145
|
-
}, async () => client, { now: Date.parse("2026-10-06T12:00:00Z") });
|
|
146
|
-
expect(received).toEqual({
|
|
147
|
-
query: '{app="app\\"name"} |= "a\\"b"',
|
|
148
|
-
options: { start: "2026-10-06T11:00:00.000Z" },
|
|
149
|
-
});
|
|
150
|
-
expect(drained).toBe(2);
|
|
151
|
-
if (mode === "json")
|
|
152
|
-
expect(output.map((text) => JSON.parse(text))).toEqual(events);
|
|
153
|
-
else {
|
|
154
|
-
if (mode === "tty")
|
|
155
|
-
expect(output[0]).toContain("line\\u001b[2J\\u000anext");
|
|
156
|
-
else
|
|
157
|
-
expect(output[0]).toContain("line\u001b[2J\nnext");
|
|
158
|
-
expect(output[1]).toContain("Logs expired");
|
|
159
|
-
expect(output[1].includes("\x1b[2m")).toBe(mode === "tty");
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
test("tail survives an actual fake-fetch disconnect through the SDK", async () => {
|
|
163
|
-
let calls = 0;
|
|
164
|
-
const cursors = [], output = [];
|
|
165
|
-
const abort = new AbortController();
|
|
166
|
-
const transport = new Transport({
|
|
167
|
-
apiKey: "fake",
|
|
168
|
-
baseUrl: "https://fake.test",
|
|
169
|
-
fetch: (async (input) => {
|
|
170
|
-
cursors.push(new URL(input instanceof Request ? input.url : input).searchParams.get("cursor"));
|
|
171
|
-
const cursor = String(++calls);
|
|
172
|
-
return new Response(`id: ${cursor}\ndata: ${JSON.stringify({ time: "now", timeUnixNano: "1", labels: {}, body: "same", severityText: "INFO" })}\n\n`, { headers: { "content-type": "text/event-stream" } });
|
|
173
|
-
}),
|
|
174
|
-
});
|
|
175
|
-
const client = { logs: logs(transport), traces: traces(transport), alerts: alerts(transport) };
|
|
176
|
-
const run = logsProductCommand("logs", ["tail", "{}"], { positional: ["tail", "{}"], flags: new Map() }, {
|
|
177
|
-
json: true,
|
|
178
|
-
write(text) {
|
|
179
|
-
output.push(text);
|
|
180
|
-
if (output.length === 2)
|
|
181
|
-
abort.abort(new Error("done"));
|
|
182
|
-
},
|
|
183
|
-
error() { },
|
|
184
|
-
}, async () => client, { signal: abort.signal });
|
|
185
|
-
await expect(run).rejects.toThrow("done");
|
|
186
|
-
expect(cursors).toEqual([null, "1"]);
|
|
187
|
-
expect(output.map((text) => JSON.parse(text).cursor)).toEqual(["1", "2"]);
|
|
188
|
-
});
|
|
189
|
-
for (const [product, positional, flags] of [
|
|
190
|
-
["logs", ["query", "{}"], { limit: "0" }],
|
|
191
|
-
["logs", ["query"], {}],
|
|
192
|
-
["logs", ["tail", "api"], { since: "forever" }],
|
|
193
|
-
["alerts", ["create"], { body: "[1]" }],
|
|
194
|
-
["alerts", ["create"], { body: "broken" }],
|
|
195
|
-
["alerts", ["silence", "id"], { until: "tomorrow" }],
|
|
196
|
-
["logs", ["ingest"], { body: "{}" }],
|
|
197
|
-
["traces", ["ingest"], { body: "{}" }],
|
|
198
|
-
["alerts", ["oops", "id"], {}],
|
|
199
|
-
])
|
|
200
|
-
test(`refuses ${positional.join(" ")} ${JSON.stringify(flags)}`, async () => {
|
|
201
|
-
const { client, requests } = setup();
|
|
202
|
-
await expect(logsProductCommand(product, positional, { positional, flags: new Map(Object.entries(flags).map(([key, value]) => [key, [value]])) }, { json: true, write() { }, error() { } }, async () => client)).rejects.toMatchObject({ code: "usage", hint: expect.any(String) });
|
|
203
|
-
expect(requests).toHaveLength(0);
|
|
204
|
-
});
|
|
205
|
-
test("help needs no credentials", async () => {
|
|
206
|
-
const output = [];
|
|
207
|
-
await logsProductCommand("logs", ["help"], { positional: ["help"], flags: new Map() }, { json: false, write: (text) => output.push(text), error() { } }, async () => {
|
|
208
|
-
throw new Error("credentials requested");
|
|
209
|
-
});
|
|
210
|
-
expect(output[0]).toContain("\nruntime logs query");
|
|
211
|
-
});
|
|
212
|
-
test("typed failures reach the main CLI's JSON error renderer unchanged", async () => {
|
|
213
|
-
const { client } = setup();
|
|
214
|
-
const failure = new InvalidRequestError({
|
|
215
|
-
code: "query_limit",
|
|
216
|
-
message: "Too broad",
|
|
217
|
-
status: 400,
|
|
218
|
-
hint: "Narrow the time range.",
|
|
219
|
-
});
|
|
220
|
-
client.logs.queryRange = async () => {
|
|
221
|
-
throw failure;
|
|
222
|
-
};
|
|
223
|
-
await expect(logsProductCommand("logs", ["query", "{}"], { positional: ["query", "{}"], flags: new Map() }, {
|
|
224
|
-
json: true,
|
|
225
|
-
write() {
|
|
226
|
-
throw new Error("Do not print success on failure");
|
|
227
|
-
},
|
|
228
|
-
error() { },
|
|
229
|
-
}, async () => client)).rejects.toBe(failure);
|
|
230
|
-
});
|
|
231
|
-
test("human JSON keeps formatting but escapes C1 terminal controls", async () => {
|
|
232
|
-
const { client } = setup();
|
|
233
|
-
const output = [];
|
|
234
|
-
client.traces.get = async () => ({ resourceSpans: [], marker: "\u009b2J" });
|
|
235
|
-
await logsProductCommand("traces", ["get", "id"], { positional: ["get", "id"], flags: new Map() }, { json: false, tty: true, write: (text) => output.push(text), error() { } }, async () => client);
|
|
236
|
-
expect(output[0]).not.toContain("\u009b");
|
|
237
|
-
expect(output[0]).toContain("\\u009b");
|
|
238
|
-
expect(output[0]).toContain("\n");
|
|
239
|
-
expect(JSON.parse(output[0]).marker).toBe("\u009b2J");
|
|
240
|
-
});
|
|
241
|
-
test("a terminal shows what customers' programs wrote, escaped, and nothing acts", async () => {
|
|
242
|
-
// Clipboard (OSC 52), a forged link (OSC 8), screen clear, the one-byte C1
|
|
243
|
-
// CSI and a right-to-left override; tab stays.
|
|
244
|
-
const hostile = "a\tb \x1b]52;c;c2VjcmV0\x07 \x1b]8;;https://evil.test\x07ok\x1b]8;;\x07 \x1b[2J \u009b2J \u202eevil";
|
|
245
|
-
const shown = terminalSafe(hostile);
|
|
246
|
-
// eslint-disable-next-line no-control-regex -- the test looks for control characters
|
|
247
|
-
expect(shown).not.toMatch(/[\x00-\x08\x0a-\x1f\x7f-\x9f\u202e]/);
|
|
248
|
-
expect(shown).toContain("a\tb");
|
|
249
|
-
expect(shown).toContain("\\u001b]52;c;c2VjcmV0\\u0007");
|
|
250
|
-
expect(shown).toContain("\\u009b2J \\u202eevil");
|
|
251
|
-
const run = async (tty, raw, json) => {
|
|
252
|
-
const { client } = setup();
|
|
253
|
-
client.logs.tail = async function* () {
|
|
254
|
-
yield { cursor: "c", time: "t", severityText: "INFO", body: hostile, labels: {} };
|
|
255
|
-
};
|
|
256
|
-
const output = [];
|
|
257
|
-
const flags = new Map(raw ? [["raw", ["true"]]] : []);
|
|
258
|
-
await logsProductCommand("logs", ["tail", "{}"], { positional: ["tail", "{}"], flags }, { json, tty, write: (text) => output.push(text), error() { } }, async () => client);
|
|
259
|
-
return output.join("\n");
|
|
260
|
-
};
|
|
261
|
-
for (const json of [false, true]) {
|
|
262
|
-
const onTerminal = await run(true, false, json);
|
|
263
|
-
expect(onTerminal).not.toContain("\x1b]");
|
|
264
|
-
expect(onTerminal).not.toContain("\u009b");
|
|
265
|
-
expect(onTerminal).not.toContain("\u202e");
|
|
266
|
-
if (json)
|
|
267
|
-
expect(JSON.parse(onTerminal).body).toBe(hostile);
|
|
268
|
-
// A pipe, or --raw on a terminal, gets exactly what was written.
|
|
269
|
-
expect(await run(false, false, json)).toContain(json ? JSON.stringify(hostile) : hostile);
|
|
270
|
-
expect(await run(true, true, json)).toContain(json ? JSON.stringify(hostile) : hostile);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|