@ornncompute/cli 0.2.24 → 1.0.25

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 (85) hide show
  1. package/README.md +298 -426
  2. package/package.json +12 -6
  3. package/src/admin-capacity-reservations.mjs +280 -0
  4. package/src/admin-command-handlers.mjs +57 -0
  5. package/src/admin-enrollment.mjs +680 -0
  6. package/src/admin-mutations.mjs +187 -0
  7. package/src/admin-node-diagnostics.mjs +412 -0
  8. package/src/admin-node-lifecycle.mjs +908 -0
  9. package/src/admin-shell.mjs +322 -0
  10. package/src/api-client.mjs +81 -28
  11. package/src/atomic-output.mjs +112 -0
  12. package/src/auth-store.mjs +182 -11
  13. package/src/capability-catalog.mjs +7 -0
  14. package/src/catalog-shell.mjs +1350 -0
  15. package/src/cli.mjs +123 -10266
  16. package/src/connect-mode-handlers.mjs +170 -0
  17. package/src/device-auth.mjs +178 -61
  18. package/src/evidence-commands.mjs +1340 -0
  19. package/src/external-bucket-commands.mjs +1374 -0
  20. package/src/interaction-runtime.mjs +310 -0
  21. package/src/kubernetes-commands.mjs +2229 -0
  22. package/src/launch-mode-handlers.mjs +119 -0
  23. package/src/machine-reference.mjs +277 -0
  24. package/src/mount-commands.mjs +1146 -0
  25. package/src/session-commands.mjs +565 -0
  26. package/src/slurm-commands.mjs +263 -0
  27. package/src/startup.mjs +34 -11
  28. package/src/storage-commands.mjs +1144 -0
  29. package/src/update.mjs +172 -24
  30. package/src/vm-commands.mjs +4273 -0
  31. package/src/vpn-secret-output.mjs +150 -0
  32. package/src/wait-runtime.mjs +231 -0
  33. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +126 -0
  34. package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
  35. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
  36. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
  37. package/vendor/capabilities/capabilities/admin.js +89 -0
  38. package/vendor/capabilities/capabilities/capacity-offers.js +368 -0
  39. package/vendor/capabilities/capabilities/cli.js +156 -0
  40. package/vendor/capabilities/capabilities/clusters.js +150 -1
  41. package/vendor/capabilities/capabilities/compute.js +522 -0
  42. package/vendor/capabilities/capabilities/evidence.js +145 -0
  43. package/vendor/capabilities/capabilities/external-buckets.js +191 -0
  44. package/vendor/capabilities/capabilities/fleet.js +21 -5
  45. package/vendor/capabilities/capabilities/identity.js +114 -3
  46. package/vendor/capabilities/capabilities/listings.js +14 -8
  47. package/vendor/capabilities/capabilities/mounts.js +176 -0
  48. package/vendor/capabilities/capabilities/networks.js +1257 -0
  49. package/vendor/capabilities/capabilities/observability.js +38 -8
  50. package/vendor/capabilities/capabilities/tokens.js +20 -8
  51. package/vendor/capabilities/capabilities/volumes.js +178 -0
  52. package/vendor/capabilities/capability.js +19 -13
  53. package/vendor/capabilities/cli-catalog.js +65 -0
  54. package/vendor/capabilities/cli-migrations.js +288 -0
  55. package/vendor/capabilities/cli.js +7 -0
  56. package/vendor/capabilities/software-catalog.js +36 -0
  57. package/vendor/capabilities/spec-catalog.js +2 -2
  58. package/src/catalog-dispatch.mjs +0 -605
  59. package/src/color.mjs +0 -71
  60. package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
  61. package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
  62. package/vendor/capabilities/capabilities/identity.d.ts +0 -3
  63. package/vendor/capabilities/capabilities/listings.d.ts +0 -51
  64. package/vendor/capabilities/capabilities/observability.d.ts +0 -5
  65. package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
  66. package/vendor/capabilities/capabilities/staff-access.js +0 -278
  67. package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
  68. package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
  69. package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
  70. package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
  71. package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
  72. package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
  73. package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
  74. package/vendor/capabilities/capabilities/staff-shared.js +0 -98
  75. package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
  76. package/vendor/capabilities/capabilities/users.d.ts +0 -2
  77. package/vendor/capabilities/capability.d.ts +0 -93
  78. package/vendor/capabilities/catalog.d.ts +0 -3
  79. package/vendor/capabilities/catalog.js +0 -26
  80. package/vendor/capabilities/index.d.ts +0 -16
  81. package/vendor/capabilities/index.js +0 -16
  82. package/vendor/capabilities/names.d.ts +0 -6
  83. package/vendor/capabilities/role.d.ts +0 -6
  84. package/vendor/capabilities/role.js +0 -17
  85. package/vendor/capabilities/spec-catalog.d.ts +0 -78
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Continue the shared `connect` journey after ticket 10 has proved private
3
+ * network and VPN readiness. Access-mode modules add ordered handlers with:
4
+ *
5
+ * { mode, matches(state), connect(state) }
6
+ *
7
+ * `connect` returns interaction-runtime result options (`data`, `outcome`, and
8
+ * optional `presentation`), not a rendered envelope. A handler must match from
9
+ * authoritative server facts; target spelling is never a mode selector.
10
+ */
11
+
12
+ function resolverErrorPriority(error) {
13
+ if (Number(error?.exitCode) === 2) return 2;
14
+ const status = Number(error?.status);
15
+ const code = typeof error?.code === "string" ? error.code.toLowerCase() : "";
16
+ if (status === 404 || /(?:not_found|not_supported|unsupported|mode_unavailable)$/.test(code)) {
17
+ return 0;
18
+ }
19
+ return 1;
20
+ }
21
+ async function validateConnectModeInput(state, handler, resolved) {
22
+ const input = state.input ?? {};
23
+ const mode = resolved?.mode ?? handler.mode;
24
+ if (mode === "vm" || mode === "bare-metal" || mode === "slurm") {
25
+ if (input.output || input.overwrite) {
26
+ state.interaction.usage(
27
+ `${mode === "slurm" ? "Slurm" : mode === "vm" ? "VM" : "Bare Metal"} connect uses native SSH and does not write a kubeconfig or other output file.`,
28
+ {
29
+ code: mode === "slurm" ? "slurm_output_unsupported" : "direct_machine_output_unsupported",
30
+ field: input.output ? "output" : "overwrite",
31
+ }
32
+ );
33
+ }
34
+ return;
35
+ }
36
+ if (mode !== "kubernetes") return;
37
+ if (input.identityFile || input.user) {
38
+ state.interaction.usage(
39
+ "Kubernetes connect writes a kubeconfig and does not accept SSH identity or user options.",
40
+ {
41
+ code: "kubernetes_ssh_options_unsupported",
42
+ field: input.identityFile ? "identityFile" : "user",
43
+ }
44
+ );
45
+ }
46
+ if (input.print === true) {
47
+ state.interaction.usage(
48
+ "Kubernetes connect writes only to an explicit --output path; ordinary output never contains credential bytes.",
49
+ {
50
+ code: "kubeconfig_output_required",
51
+ field: "output",
52
+ }
53
+ );
54
+ }
55
+ if (
56
+ !input.output &&
57
+ input.guided === true &&
58
+ state.interaction?.eligibleHuman === true &&
59
+ state.interaction?.machine !== true
60
+ ) {
61
+ input.output = await state.interaction.ask({ label: "Kubeconfig output path" });
62
+ }
63
+ if (!input.output) {
64
+ state.interaction.usage("Kubernetes connect requires --output <path>.", {
65
+ code: "kubeconfig_output_required",
66
+ field: "output",
67
+ });
68
+ }
69
+ }
70
+
71
+ export async function resolveConnectTargetWithHandlers(state, handlers = []) {
72
+ for (const handler of handlers) {
73
+ if (
74
+ !handler ||
75
+ typeof handler.matches !== "function" ||
76
+ typeof handler.connect !== "function"
77
+ ) {
78
+ throw new TypeError("Connect mode handlers require matches(state) and connect(state).");
79
+ }
80
+ if (handler.resolve !== undefined && typeof handler.resolve !== "function") {
81
+ throw new TypeError("A connect mode handler resolve member must be a function.");
82
+ }
83
+ if (handler.candidates !== undefined && typeof handler.candidates !== "function") {
84
+ throw new TypeError("A connect mode handler candidates member must be a function.");
85
+ }
86
+ }
87
+
88
+ if (
89
+ !state.reference &&
90
+ state.interaction?.eligibleHuman === true &&
91
+ state.interaction?.machine !== true
92
+ ) {
93
+ const candidates = [];
94
+ for (const handler of handlers) {
95
+ if (typeof handler.candidates !== "function") continue;
96
+ const available = await handler.candidates(state);
97
+ if (!Array.isArray(available)) {
98
+ throw new TypeError("A connect mode handler candidates member must return an array.");
99
+ }
100
+ for (const candidate of available) {
101
+ if (
102
+ !candidate ||
103
+ typeof candidate.label !== "string" ||
104
+ typeof candidate.reference !== "string"
105
+ ) {
106
+ throw new TypeError("Connect candidates require label and reference strings.");
107
+ }
108
+ candidates.push({ ...candidate, handler });
109
+ }
110
+ }
111
+ if (candidates.length > 0) {
112
+ let selected = candidates[0];
113
+ if (candidates.length > 1) {
114
+ const value = await state.interaction.ask({
115
+ label: "Compute target",
116
+ choices: candidates.map((candidate, index) => ({
117
+ label: candidate.label,
118
+ value: String(index),
119
+ })),
120
+ });
121
+ selected = candidates[Number(value)];
122
+ if (!selected) {
123
+ state.interaction.usage("Choose one exact compute target.", {
124
+ code: "connect_target_required",
125
+ });
126
+ }
127
+ }
128
+ const resolved = await selected.handler.resolve({
129
+ ...state,
130
+ reference: selected.reference,
131
+ });
132
+ if (resolved) await validateConnectModeInput(state, selected.handler, resolved);
133
+ return resolved;
134
+ }
135
+ }
136
+
137
+ let strongestError = null;
138
+ let strongestPriority = 0;
139
+ for (const handler of handlers) {
140
+ if (handler.resolve === undefined) continue;
141
+ try {
142
+ const resolved = await handler.resolve(state);
143
+ if (resolved) {
144
+ await validateConnectModeInput(state, handler, resolved);
145
+ return resolved;
146
+ }
147
+ } catch (error) {
148
+ const priority = resolverErrorPriority(error);
149
+ if (priority > strongestPriority) {
150
+ strongestError = error;
151
+ strongestPriority = priority;
152
+ }
153
+ }
154
+ }
155
+ if (strongestError) throw strongestError;
156
+ return null;
157
+ }
158
+ export async function continueConnectWithHandlers(state, handlers = []) {
159
+ for (const handler of handlers) {
160
+ if (
161
+ !handler ||
162
+ typeof handler.matches !== "function" ||
163
+ typeof handler.connect !== "function"
164
+ ) {
165
+ throw new TypeError("Connect mode handlers require matches(state) and connect(state).");
166
+ }
167
+ if (await handler.matches(state)) return handler.connect(state);
168
+ }
169
+ return null;
170
+ }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
 
3
3
  import { configuredAuthBaseUrl } from "./auth-store.mjs";
4
+ import { readUntilDeadline, waitForDelay } from "./wait-runtime.mjs";
4
5
 
5
6
  const DEFAULT_AUTH_BASE_URL = "http://localhost:3000";
6
7
  const DEFAULT_POLL_INTERVAL_SECONDS = 2;
@@ -27,18 +28,61 @@ export function resolveAuthBaseUrl({ explicit, env = process.env } = {}) {
27
28
  return value.replace(/\/+$/, "");
28
29
  }
29
30
 
30
- export async function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) {
31
- const command =
32
- platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
33
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
31
+ function browserUrl(value, expectedBaseUrl) {
32
+ if (
33
+ typeof value !== "string" ||
34
+ value !== value.trim() ||
35
+ /[\u0000-\u001f\u007f]|%(?:0[0-9a-f]|1[0-9a-f]|7f)/i.test(value)
36
+ ) {
37
+ throw new DeviceAuthError("Auth server returned an unsafe browser URL.");
38
+ }
39
+ let parsed;
40
+ try {
41
+ parsed = new URL(value);
42
+ } catch {
43
+ throw new DeviceAuthError("Auth server returned an invalid browser URL.");
44
+ }
45
+ if (
46
+ !["http:", "https:"].includes(parsed.protocol) ||
47
+ parsed.username ||
48
+ parsed.password ||
49
+ (expectedBaseUrl && parsed.origin !== new URL(expectedBaseUrl).origin)
50
+ ) {
51
+ throw new DeviceAuthError("Auth server returned an unexpected browser URL.");
52
+ }
53
+ return parsed.href;
54
+ }
34
55
 
56
+ export async function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) {
35
57
  try {
58
+ const target = browserUrl(url);
59
+ const command =
60
+ platform === "darwin" ? "open" : platform === "win32" ? "powershell.exe" : "xdg-open";
61
+ // The script is constant. The URL is environment data, never cmd.exe or
62
+ // PowerShell source, even when its query contains shell metacharacters.
63
+ const script = "Start-Process -FilePath $env:ORNN_BROWSER_URL";
64
+ const args =
65
+ platform === "win32"
66
+ ? [
67
+ "-NoProfile",
68
+ "-NonInteractive",
69
+ "-EncodedCommand",
70
+ Buffer.from(script, "utf16le").toString("base64"),
71
+ ]
72
+ : [target];
36
73
  const child = spawnImpl(command, args, {
37
74
  detached: true,
38
75
  stdio: "ignore",
76
+ shell: false,
77
+ ...(platform === "win32" ? { env: { ...process.env, ORNN_BROWSER_URL: target } } : {}),
78
+ });
79
+ return await new Promise((resolve) => {
80
+ child.once("error", () => resolve(false));
81
+ child.once("spawn", () => {
82
+ child.unref?.();
83
+ resolve(true);
84
+ });
39
85
  });
40
- child.unref?.();
41
- return true;
42
86
  } catch {
43
87
  return false;
44
88
  }
@@ -50,9 +94,14 @@ export async function startDeviceFlow({ apiBaseUrl, authBaseUrl, fetchImpl = fet
50
94
 
51
95
  const deviceCode = pickString(data, "deviceCode", "device_code");
52
96
  const userCode = pickString(data, "userCode", "user_code");
53
- const verificationUri =
54
- pickString(data, "verificationUriComplete", "verification_uri_complete") ||
55
- pickString(data, "verificationUri", "verification_uri");
97
+ const verificationUri = [
98
+ "verificationUriComplete",
99
+ "verification_uri_complete",
100
+ "verificationUri",
101
+ "verification_uri",
102
+ ]
103
+ .map((key) => data?.[key])
104
+ .find((value) => typeof value === "string" && value.length > 0);
56
105
 
57
106
  if (!deviceCode || !userCode || !verificationUri) {
58
107
  throw new DeviceAuthError("Auth server returned an invalid device login response.", {
@@ -65,15 +114,24 @@ export async function startDeviceFlow({ apiBaseUrl, authBaseUrl, fetchImpl = fet
65
114
  expiresIn: pickNumber(data, "expiresIn", "expires_in") ?? DEFAULT_TIMEOUT_SECONDS,
66
115
  interval: pickNumber(data, "interval") ?? DEFAULT_POLL_INTERVAL_SECONDS,
67
116
  userCode,
68
- verificationUri,
117
+ verificationUri: browserUrl(verificationUri, authBaseUrl || apiBaseUrl),
69
118
  };
70
119
  }
71
120
 
72
- export async function pollDeviceFlow({ apiBaseUrl, authBaseUrl, deviceCode, fetchImpl = fetch } = {}) {
121
+ export async function pollDeviceFlow({
122
+ apiBaseUrl,
123
+ authBaseUrl,
124
+ deviceCode,
125
+ fetchImpl = fetch,
126
+ signal,
127
+ } = {}) {
73
128
  const requestBaseUrl = apiBaseUrl || authBaseUrl;
74
- return postJson(fetchImpl, `${requestBaseUrl}/v1/cli/device/poll`, {
75
- device_code: deviceCode,
76
- });
129
+ return postJson(
130
+ fetchImpl,
131
+ `${requestBaseUrl}/v1/cli/device/poll`,
132
+ { device_code: deviceCode },
133
+ { signal }
134
+ );
77
135
  }
78
136
 
79
137
  export async function waitForDeviceApproval({
@@ -83,60 +141,124 @@ export async function waitForDeviceApproval({
83
141
  expiresIn,
84
142
  fetchImpl = fetch,
85
143
  interval,
144
+ now = () => Date.now(),
86
145
  onPending,
146
+ signal: suppliedSignal,
147
+ sleepImpl,
87
148
  timeoutSeconds,
88
149
  } = {}) {
89
- const pollIntervalMs = Math.max(1, Number(interval) || DEFAULT_POLL_INTERVAL_SECONDS) * 1000;
90
- const timeoutMs =
91
- Math.max(1, Number(timeoutSeconds) || Number(expiresIn) || DEFAULT_TIMEOUT_SECONDS) * 1000;
92
- const deadline = Date.now() + timeoutMs;
93
-
94
- while (Date.now() < deadline) {
95
- await sleep(pollIntervalMs);
96
- const data = await pollDeviceFlow({ apiBaseUrl, authBaseUrl, deviceCode, fetchImpl });
97
- const status = pickString(data, "status") || "pending";
98
-
99
- if (status === "pending") {
100
- onPending?.();
101
- continue;
102
- }
150
+ const suppliedInterval = Number(interval);
151
+ const suppliedTimeout = Number(timeoutSeconds ?? expiresIn);
152
+ const pollIntervalMs = Math.max(
153
+ 1,
154
+ (Number.isFinite(suppliedInterval) && suppliedInterval > 0
155
+ ? suppliedInterval
156
+ : DEFAULT_POLL_INTERVAL_SECONDS) * 1_000
157
+ );
158
+ const timeoutMs = Math.max(
159
+ 1,
160
+ (Number.isFinite(suppliedTimeout) && suppliedTimeout > 0
161
+ ? suppliedTimeout
162
+ : DEFAULT_TIMEOUT_SECONDS) * 1_000
163
+ );
164
+ const startedAt = now();
165
+ let scheduledElapsedMs = 0;
166
+ const localController = suppliedSignal ? null : new AbortController();
167
+ const signal = suppliedSignal ?? localController.signal;
168
+ const onSigint = localController ? () => localController.abort() : null;
169
+ if (onSigint) process.once("SIGINT", onSigint);
170
+
171
+ const remainingTime = () =>
172
+ timeoutMs - Math.max(scheduledElapsedMs, Math.max(0, now() - startedAt));
173
+
174
+ try {
175
+ for (;;) {
176
+ const remainingBeforeSleep = remainingTime();
177
+ if (remainingBeforeSleep <= 0) {
178
+ throw new DeviceAuthError("Timed out waiting for browser login approval.");
179
+ }
180
+ const delay = Math.min(pollIntervalMs, remainingBeforeSleep);
181
+ const waited = await waitForDelay(delay, sleepImpl, signal);
182
+ if (waited === "interrupted" || signal.aborted) {
183
+ throw new DeviceAuthError("Browser login wait was interrupted.", {
184
+ detail: { code: "interrupted" },
185
+ });
186
+ }
187
+ scheduledElapsedMs += delay;
188
+ const remaining = remainingTime();
189
+ if (remaining <= 0) {
190
+ throw new DeviceAuthError("Timed out waiting for browser login approval.");
191
+ }
192
+ const polled = await readUntilDeadline(
193
+ ({ signal: requestSignal }) =>
194
+ pollDeviceFlow({
195
+ apiBaseUrl,
196
+ authBaseUrl,
197
+ deviceCode,
198
+ fetchImpl,
199
+ signal: requestSignal,
200
+ }),
201
+ { signal, timeoutMs: remaining }
202
+ );
203
+ if (polled.kind === "interrupted") {
204
+ throw new DeviceAuthError("Browser login wait was interrupted.", {
205
+ detail: { code: "interrupted" },
206
+ });
207
+ }
208
+ if (polled.kind === "timeout") {
209
+ throw new DeviceAuthError("Timed out waiting for browser login approval.");
210
+ }
211
+ const data = polled.value;
212
+ const status = pickString(data, "status") || "pending";
213
+
214
+ if (status === "pending") {
215
+ onPending?.();
216
+ continue;
217
+ }
218
+
219
+ if (status === "approved") {
220
+ const token = pickString(
221
+ data,
222
+ "accessToken",
223
+ "access_token",
224
+ "sessionToken",
225
+ "session_token"
226
+ );
227
+ const user = data.user && typeof data.user === "object" ? data.user : null;
103
228
 
104
- if (status === "approved") {
105
- const token = pickString(data, "accessToken", "access_token", "sessionToken", "session_token");
106
- const user = data.user && typeof data.user === "object" ? data.user : null;
229
+ if (!token || !user?.email) {
230
+ throw new DeviceAuthError("Auth server approved login without a usable session.");
231
+ }
107
232
 
108
- if (!token || !user?.email) {
109
- throw new DeviceAuthError("Auth server approved login without a usable session.");
233
+ return {
234
+ accessToken: token,
235
+ apiBaseUrl: apiBaseUrl || authBaseUrl,
236
+ authBaseUrl,
237
+ tokenType: pickString(data, "tokenType", "token_type") || "bearer",
238
+ user: {
239
+ email: String(user.email),
240
+ id: user.id ? String(user.id) : null,
241
+ name: user.name ? String(user.name) : null,
242
+ },
243
+ };
110
244
  }
111
245
 
112
- return {
113
- accessToken: token,
114
- apiBaseUrl: apiBaseUrl || authBaseUrl,
115
- authBaseUrl,
116
- tokenType: pickString(data, "tokenType", "token_type") || "bearer",
117
- user: {
118
- email: String(user.email),
119
- id: user.id ? String(user.id) : null,
120
- name: user.name ? String(user.name) : null,
121
- },
122
- };
123
- }
246
+ if (status === "denied") {
247
+ throw new DeviceAuthError("Device login was denied in the browser.");
248
+ }
124
249
 
125
- if (status === "denied") {
126
- throw new DeviceAuthError("Device login was denied in the browser.");
127
- }
250
+ if (status === "expired") {
251
+ throw new DeviceAuthError("Device login expired. Run `ornn login` again.");
252
+ }
128
253
 
129
- if (status === "expired") {
130
- throw new DeviceAuthError("Device login expired. Run `ornn login` again.");
254
+ throw new DeviceAuthError(`Auth server returned unexpected status: ${status}`);
131
255
  }
132
-
133
- throw new DeviceAuthError(`Auth server returned unexpected status: ${status}`);
256
+ } finally {
257
+ if (onSigint) process.removeListener("SIGINT", onSigint);
134
258
  }
135
-
136
- throw new DeviceAuthError("Timed out waiting for browser login approval.");
137
259
  }
138
260
 
139
- async function postJson(fetchImpl, url, body) {
261
+ async function postJson(fetchImpl, url, body, { signal } = {}) {
140
262
  const response = await fetchImpl(url, {
141
263
  body: JSON.stringify(body),
142
264
  headers: {
@@ -144,6 +266,7 @@ async function postJson(fetchImpl, url, body) {
144
266
  "Content-Type": "application/json",
145
267
  },
146
268
  method: "POST",
269
+ ...(signal ? { signal } : {}),
147
270
  });
148
271
  const text = await response.text();
149
272
  const data = text ? parseJson(text, url) : {};
@@ -186,9 +309,3 @@ function pickNumber(value, ...keys) {
186
309
  }
187
310
  return null;
188
311
  }
189
-
190
- function sleep(ms) {
191
- return new Promise((resolve) => {
192
- setTimeout(resolve, ms);
193
- });
194
- }