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.
Files changed (50) hide show
  1. package/README.md +3 -3
  2. package/README.src.md +2 -2
  3. package/dist/billing-cli.js +8 -4
  4. package/dist/blaxel/sandbox.d.ts +1 -1
  5. package/dist/blaxel/sandbox.js +1 -1
  6. package/dist/cli.d.ts +17 -2
  7. package/dist/cli.js +242 -69
  8. package/dist/credentials.d.ts +3 -0
  9. package/dist/credentials.js +15 -0
  10. package/dist/daytona/daytona.d.ts +1 -1
  11. package/dist/daytona/daytona.js +1 -1
  12. package/dist/e2b/sandbox.d.ts +1 -1
  13. package/dist/errors.d.ts +1 -1
  14. package/dist/errors.js +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/machine.d.ts +15 -1
  17. package/dist/machine.js +26 -11
  18. package/dist/mcp.js +144 -20
  19. package/dist/products/api-keys.d.ts +73 -0
  20. package/dist/products/api-keys.js +23 -0
  21. package/dist/products/apps.d.ts +12 -31
  22. package/dist/products/apps.js +8 -113
  23. package/dist/products/balancers.d.ts +87 -0
  24. package/dist/products/balancers.js +8 -0
  25. package/dist/products/builds.d.ts +30 -0
  26. package/dist/products/builds.js +6 -0
  27. package/dist/products/index.d.ts +2 -0
  28. package/dist/products/index.js +2 -0
  29. package/dist/products/jobs.d.ts +2 -2
  30. package/dist/products/jobs.js +1 -1
  31. package/dist/products/observability.d.ts +7 -2
  32. package/dist/products/resources.d.ts +54 -0
  33. package/dist/products/resources.js +122 -0
  34. package/dist/products/secrets.d.ts +22 -0
  35. package/dist/products/secrets.js +8 -0
  36. package/dist/products/volumes.d.ts +3 -0
  37. package/dist/sandbox.d.ts +4 -0
  38. package/dist/sandbox.js +27 -2
  39. package/dist/transport.d.ts +4 -1
  40. package/dist/transport.js +25 -4
  41. package/dist/types.d.ts +9 -5
  42. package/dist/vercel/sandbox.d.ts +1 -1
  43. package/dist/vercel/sandbox.js +1 -1
  44. package/dist/wallet.d.ts +4 -1
  45. package/dist/wallet.js +2 -1
  46. package/package.json +2 -14
  47. package/dist/cli/logs.test.d.ts +0 -1
  48. package/dist/cli/logs.test.js +0 -272
  49. package/dist/products/logs.test.d.ts +0 -1
  50. 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.12.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
- this.baseUrl = apiOrigin(options.baseUrl ? reachable(options.baseUrl) : defaultBaseUrl());
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.#rest(pause, signal, gate);
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.#rest(pause, signal, gate);
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 included usage while it lasts, then prepaid credit. */
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 included usage every month only. `hours`: free hours
299
- * given before 5 October 2026, kept, beside the included usage. Absent
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 included usage renews: the 1st of next month, 00:00 UTC. */
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 included usage
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. */
@@ -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 included usage first, then prepaid credit, exactly as withruntime's own create. */
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`. */
@@ -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 included usage first, then prepaid credit, exactly as withruntime's own create. */
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 $10,000).
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 $10,000).
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.12.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 scripts/test.ts",
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",
@@ -1 +0,0 @@
1
- export {};
@@ -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 {};