@skrr-ai/cli 0.1.26 → 0.1.27
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/dist/base-command.js +25 -0
- package/dist/commands/machines/dedicated/attach.js +4 -3
- package/dist/commands/machines/dedicated/current.d.ts +19 -0
- package/dist/commands/machines/dedicated/current.js +49 -0
- package/dist/commands/machines/dedicated/destroy.js +7 -6
- package/dist/commands/machines/dedicated/exec.d.ts +4 -0
- package/dist/commands/machines/dedicated/exec.js +19 -2
- package/dist/commands/machines/dedicated/grow.js +3 -2
- package/dist/commands/machines/dedicated/health-check.js +4 -3
- package/dist/commands/machines/dedicated/incident-close.js +3 -2
- package/dist/commands/machines/dedicated/index.js +2 -0
- package/dist/commands/machines/dedicated/restart.js +3 -2
- package/dist/commands/machines/dedicated/restore.d.ts +26 -0
- package/dist/commands/machines/dedicated/restore.js +64 -0
- package/dist/commands/machines/dedicated/show.js +3 -2
- package/dist/commands/machines/dedicated/snapshot.js +3 -2
- package/dist/commands/machines/dedicated/spending-limit.js +4 -3
- package/dist/commands/machines/dedicated/start.js +3 -2
- package/dist/commands/machines/dedicated/stop.js +3 -2
- package/dist/lib/dedicated-exec.d.ts +80 -0
- package/dist/lib/dedicated-exec.js +271 -0
- package/dist/lib/dedicated-lease-command.d.ts +9 -0
- package/dist/lib/dedicated-lease-command.js +24 -1
- package/dist/lib/dedicated-machines.d.ts +90 -1
- package/dist/lib/dedicated-machines.js +122 -5
- package/dist/lib/edge-error-page.d.ts +49 -0
- package/dist/lib/edge-error-page.js +120 -0
- package/dist/lib/harnesses.d.ts +9 -1
- package/dist/lib/harnesses.js +20 -2
- package/dist/lib/hosted-machines.js +7 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +12 -0
- package/oclif.manifest.json +3341 -3147
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
|
|
3
|
+
exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
|
|
4
4
|
exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
|
|
5
5
|
exports.isDedicatedRepositoryRef = isDedicatedRepositoryRef;
|
|
6
6
|
exports.createDedicatedRuntime = createDedicatedRuntime;
|
|
@@ -8,10 +8,13 @@ exports.listDedicatedRuntimes = listDedicatedRuntimes;
|
|
|
8
8
|
exports.getDedicatedRuntime = getDedicatedRuntime;
|
|
9
9
|
exports.performDedicatedLeaseAction = performDedicatedLeaseAction;
|
|
10
10
|
exports.growDedicatedRuntime = growDedicatedRuntime;
|
|
11
|
+
exports.restoreDeletedDedicatedRuntime = restoreDeletedDedicatedRuntime;
|
|
11
12
|
exports.setDedicatedRuntimeSpendingLimit = setDedicatedRuntimeSpendingLimit;
|
|
12
13
|
exports.closeDedicatedRuntimeIncidentHold = closeDedicatedRuntimeIncidentHold;
|
|
13
14
|
exports.destroyDedicatedRuntime = destroyDedicatedRuntime;
|
|
14
15
|
exports.execDedicatedRuntime = execDedicatedRuntime;
|
|
16
|
+
exports.startDedicatedRuntimeExecution = startDedicatedRuntimeExecution;
|
|
17
|
+
exports.observeDedicatedRuntimeExecution = observeDedicatedRuntimeExecution;
|
|
15
18
|
exports.readDedicatedRuntimeFileChunk = readDedicatedRuntimeFileChunk;
|
|
16
19
|
exports.writeDedicatedRuntimeFileChunk = writeDedicatedRuntimeFileChunk;
|
|
17
20
|
exports.parseDedicatedCopyEndpoints = parseDedicatedCopyEndpoints;
|
|
@@ -20,14 +23,17 @@ exports.dedicatedLeaseState = dedicatedLeaseState;
|
|
|
20
23
|
exports.dedicatedLeaseDisplayName = dedicatedLeaseDisplayName;
|
|
21
24
|
exports.formatUsdCents = formatUsdCents;
|
|
22
25
|
exports.dedicatedSpendSummary = dedicatedSpendSummary;
|
|
26
|
+
exports.describeDedicatedDeletion = describeDedicatedDeletion;
|
|
23
27
|
exports.describeDedicatedLease = describeDedicatedLease;
|
|
24
28
|
exports.dedicatedLeaseHarnesses = dedicatedLeaseHarnesses;
|
|
29
|
+
exports.dedicatedLeaseIdForDaemon = dedicatedLeaseIdForDaemon;
|
|
25
30
|
exports.dedicatedTerminalHarness = dedicatedTerminalHarness;
|
|
26
31
|
exports.selectDedicatedAttachHarness = selectDedicatedAttachHarness;
|
|
27
32
|
exports.dedicatedCatalogFromProducts = dedicatedCatalogFromProducts;
|
|
28
33
|
exports.dedicatedRuntimeApiErrorCode = dedicatedRuntimeApiErrorCode;
|
|
29
34
|
exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
|
|
30
35
|
const api_fetch_1 = require("./api-fetch");
|
|
36
|
+
const edge_error_page_1 = require("./edge-error-page");
|
|
31
37
|
exports.DEDICATED_LEASE_ACTIONS = [
|
|
32
38
|
'start',
|
|
33
39
|
'stop',
|
|
@@ -85,11 +91,20 @@ function isDedicatedRepositoryRef(value) {
|
|
|
85
91
|
// unreachable by domain-api-parity while being perfectly reachable, which is the
|
|
86
92
|
// reading that trains people to allowlist things that are fine. Same shape as
|
|
87
93
|
// `withQuery` on the `request.*` side.
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
//
|
|
95
|
+
// Query parameters therefore go in `query`, never into the literal: the coverage
|
|
96
|
+
// reader matches the literal against the route table, and a `?name=` inside it
|
|
97
|
+
// makes the endpoint read as unreachable.
|
|
98
|
+
function withDedicatedScope(path, scope, query = {}) {
|
|
99
|
+
const params = Object.entries(query);
|
|
100
|
+
if (scope.workspaceId)
|
|
101
|
+
params.push(['workspaceId', scope.workspaceId]);
|
|
102
|
+
if (params.length === 0)
|
|
90
103
|
return path;
|
|
91
104
|
const separator = path.includes('?') ? '&' : '?';
|
|
92
|
-
return `${path}${separator}
|
|
105
|
+
return `${path}${separator}${params
|
|
106
|
+
.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
|
|
107
|
+
.join('&')}`;
|
|
93
108
|
}
|
|
94
109
|
async function createDedicatedRuntime(input) {
|
|
95
110
|
const { requestId, workspaceId, ...body } = input;
|
|
@@ -117,6 +132,25 @@ async function growDedicatedRuntime(leaseId, storageGb, options = {}) {
|
|
|
117
132
|
...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
|
|
118
133
|
});
|
|
119
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Restore a deleted Dedicated Runtime into a NEW machine, from the final recovery
|
|
137
|
+
* point its deletion kept. Only inside the deletion's recovery window, and once
|
|
138
|
+
* per deletion; the response's `lease` is the new machine. The cap defaults to
|
|
139
|
+
* the deleted machine's.
|
|
140
|
+
*/
|
|
141
|
+
async function restoreDeletedDedicatedRuntime(leaseId, options = {}) {
|
|
142
|
+
const body = {};
|
|
143
|
+
if (options.displayName)
|
|
144
|
+
body.displayName = options.displayName;
|
|
145
|
+
if (options.monthlySpendingLimitCents !== undefined) {
|
|
146
|
+
body.monthlySpendingLimitCents = options.monthlySpendingLimitCents;
|
|
147
|
+
}
|
|
148
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/restore`, options), {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body,
|
|
151
|
+
...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
120
154
|
/**
|
|
121
155
|
* Change the monthly spending cap of one lease.
|
|
122
156
|
*
|
|
@@ -143,8 +177,16 @@ async function destroyDedicatedRuntime(leaseId, options = {}) {
|
|
|
143
177
|
}
|
|
144
178
|
/** The server's ceiling for one exec; documented beside the route. */
|
|
145
179
|
exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = 10 * 60 * 1000;
|
|
180
|
+
/** The server's default when a command names no timeout. */
|
|
181
|
+
exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
146
182
|
/**
|
|
147
|
-
* Run one command on the machine as the workload user.
|
|
183
|
+
* Run one command on the machine as the workload user, in ONE request.
|
|
184
|
+
*
|
|
185
|
+
* The synchronous route, kept for servers that predate `executions`. It holds
|
|
186
|
+
* the request open while the command runs, so behind CloudFront a command that
|
|
187
|
+
* runs past ~60 s is cut off at the edge while it keeps running (OSK-8705).
|
|
188
|
+
* `runDedicatedExec` (dedicated-exec.ts) starts and observes instead, and falls
|
|
189
|
+
* back to this only when the server has no `executions` route.
|
|
148
190
|
*
|
|
149
191
|
* Not idempotent and not retried: `apiFetch` gives each call a fresh
|
|
150
192
|
* idempotency key, which is the rule for an unfenced write — running the same
|
|
@@ -160,6 +202,27 @@ async function execDedicatedRuntime(leaseId, input, scope = {}) {
|
|
|
160
202
|
},
|
|
161
203
|
});
|
|
162
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Start one command. `requestId` is the idempotency key: the same id, for the
|
|
207
|
+
* same command on the same lease, returns the execution it already started —
|
|
208
|
+
* so a retry after a lost response never runs the command twice.
|
|
209
|
+
*/
|
|
210
|
+
async function startDedicatedRuntimeExecution(leaseId, input, options) {
|
|
211
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/executions`, options), {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
body: {
|
|
214
|
+
command: input.command,
|
|
215
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
216
|
+
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
217
|
+
},
|
|
218
|
+
headers: { 'X-Idempotency-Key': options.requestId },
|
|
219
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/** Read a started command, letting the server hold the answer up to `waitMs` for it to end. */
|
|
223
|
+
async function observeDedicatedRuntimeExecution(leaseId, executionId, options) {
|
|
224
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/executions/${encodeURIComponent(executionId)}`, options, { waitMs: String(Math.max(0, Math.floor(options.waitMs))) }), options.signal ? { signal: options.signal } : {});
|
|
225
|
+
}
|
|
163
226
|
/** Mirrors the daemon's bounds (daemon/src/tools/file-transfer.ts). */
|
|
164
227
|
exports.DEDICATED_FILE_CHUNK_BYTES = 512 * 1024;
|
|
165
228
|
exports.DEDICATED_FILE_MAX_BYTES = 64 * 1024 * 1024;
|
|
@@ -234,6 +297,22 @@ function dedicatedSpendSummary(lease) {
|
|
|
234
297
|
* lease that is still `requested` has no resources, daemon or spend yet, and a
|
|
235
298
|
* missing value is omitted rather than printed as a column of dashes.
|
|
236
299
|
*/
|
|
300
|
+
/** One line saying what a deletion still allows. */
|
|
301
|
+
function describeDedicatedDeletion(deletion, leaseId) {
|
|
302
|
+
if (deletion.restoredToLeaseId) {
|
|
303
|
+
return `restored into ${deletion.restoredToLeaseId}`;
|
|
304
|
+
}
|
|
305
|
+
if (deletion.status === 'completed') {
|
|
306
|
+
return `permanently deleted${deletion.completedAt ? ` ${deletion.completedAt}` : ''}; no copy remains`;
|
|
307
|
+
}
|
|
308
|
+
if (deletion.status === 'purging') {
|
|
309
|
+
return 'recovery window closed; every copy is being deleted';
|
|
310
|
+
}
|
|
311
|
+
if (deletion.restorable) {
|
|
312
|
+
return `restorable until ${deletion.recoverableUntil} — \`skrr machines dedicated restore ${leaseId || '<lease-id>'}\``;
|
|
313
|
+
}
|
|
314
|
+
return 'nothing to restore; every copy is deleted when the window closes';
|
|
315
|
+
}
|
|
237
316
|
function describeDedicatedLease(lease, harnesses = []) {
|
|
238
317
|
const lines = [dedicatedLeaseDisplayName(lease)];
|
|
239
318
|
const row = (label, value) => {
|
|
@@ -291,6 +370,11 @@ function describeDedicatedLease(lease, harnesses = []) {
|
|
|
291
370
|
.filter(Boolean)
|
|
292
371
|
.join(' · '));
|
|
293
372
|
}
|
|
373
|
+
const deletion = storage.deletion;
|
|
374
|
+
if (deletion?.status) {
|
|
375
|
+
row('deleted', describeDedicatedDeletion(deletion, dedicatedLeaseId(lease)));
|
|
376
|
+
}
|
|
377
|
+
row('restored from', lease.recovery?.restoredFromLeaseId);
|
|
294
378
|
const hold = lease.recovery?.incidentHold;
|
|
295
379
|
if (hold?.status === 'open') {
|
|
296
380
|
row('incident', `hold open since ${hold.heldAt || '?'} — the failed source volume is billed until \`incident-close\``);
|
|
@@ -328,6 +412,29 @@ function dedicatedLeaseHarnesses(lease, harnesses) {
|
|
|
328
412
|
})
|
|
329
413
|
.sort((a, b) => seenAt(b) - seenAt(a));
|
|
330
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* The lease of the machine this process is running on, from the harness rows.
|
|
417
|
+
*
|
|
418
|
+
* Nothing in a workload process's environment names its lease — the daemon's
|
|
419
|
+
* environment (lease, generation, credentials) sits on the other side of the uid
|
|
420
|
+
* split by design. The CLI hand-off descriptor the daemon publishes to the
|
|
421
|
+
* workload group does carry the daemon's id, and every harness row that daemon
|
|
422
|
+
* registered carries `metadata.machineLeaseId`. Joining the two answers "which
|
|
423
|
+
* machine am I" with no new daemon surface.
|
|
424
|
+
*/
|
|
425
|
+
function dedicatedLeaseIdForDaemon(daemonId, harnesses) {
|
|
426
|
+
if (!daemonId)
|
|
427
|
+
return null;
|
|
428
|
+
const leaseIds = new Set(harnesses
|
|
429
|
+
.filter((harness) => harness.daemonId === daemonId &&
|
|
430
|
+
harness.metadata?.machineProduct === exports.DEDICATED_MACHINE_PRODUCT &&
|
|
431
|
+
typeof harness.metadata?.machineLeaseId === 'string')
|
|
432
|
+
.map((harness) => String(harness.metadata?.machineLeaseId)));
|
|
433
|
+
// One daemon serves one lease. Two would mean rows from a replaced
|
|
434
|
+
// generation disagree, and guessing between them is how `self` would act on
|
|
435
|
+
// the wrong machine.
|
|
436
|
+
return leaseIds.size === 1 ? [...leaseIds][0] : null;
|
|
437
|
+
}
|
|
331
438
|
/**
|
|
332
439
|
* The row an interactive terminal opens on: online, local, with a daemon.
|
|
333
440
|
*
|
|
@@ -485,6 +592,10 @@ function dedicatedRuntimeApiErrorCode(err) {
|
|
|
485
592
|
if (!(err instanceof api_fetch_1.ApiFetchError)) {
|
|
486
593
|
return undefined;
|
|
487
594
|
}
|
|
595
|
+
const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
|
|
596
|
+
if (edge) {
|
|
597
|
+
return edge.code;
|
|
598
|
+
}
|
|
488
599
|
try {
|
|
489
600
|
const parsed = JSON.parse(err.body || '');
|
|
490
601
|
if (typeof parsed.code === 'string' && parsed.code) {
|
|
@@ -503,6 +614,12 @@ function formatDedicatedRuntimeApiError(err) {
|
|
|
503
614
|
if (err.status === 401) {
|
|
504
615
|
return null;
|
|
505
616
|
}
|
|
617
|
+
// An HTML page is the edge answering in the API's place — a WAF block or a
|
|
618
|
+
// gateway timeout. Printing its markup told nobody anything (OSK-8700).
|
|
619
|
+
const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
|
|
620
|
+
if (edge) {
|
|
621
|
+
return edge.message;
|
|
622
|
+
}
|
|
506
623
|
const notFound = describeLeaseNotFound(err);
|
|
507
624
|
if (notFound) {
|
|
508
625
|
return notFound;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edge-error-page.ts — say what an HTML error page from the EDGE means, instead
|
|
3
|
+
* of printing it.
|
|
4
|
+
*
|
|
5
|
+
* Every API route answers in JSON. An HTML page in an error response therefore
|
|
6
|
+
* did not come from the API: it is CloudFront, its web application firewall, or
|
|
7
|
+
* a load balancer answering in the API's place. Printed as-is it is a screen of
|
|
8
|
+
* markup whose one useful line ("Request blocked", a request id) is buried, and
|
|
9
|
+
* treated as an API answer it is misleading in a specific way — a 403 reads as
|
|
10
|
+
* "you lack permission", and CloudFront's 504 page tells the reader to "try again
|
|
11
|
+
* later", which for a write that is still running repeats it.
|
|
12
|
+
*
|
|
13
|
+
* Both happened to `skrr machines dedicated exec` (OSK-8700, OSK-8705): a shell
|
|
14
|
+
* command containing `http://10.0.0.10/` came back as CloudFront's raw
|
|
15
|
+
* "403 ERROR / Request blocked" page, and a command running past 60 seconds as
|
|
16
|
+
* its raw 504 page while it kept running on the machine.
|
|
17
|
+
*
|
|
18
|
+
* General on purpose: nothing here knows which command asked.
|
|
19
|
+
*/
|
|
20
|
+
export type EdgeErrorCode = 'EDGE_REQUEST_BLOCKED' | 'EDGE_GATEWAY_TIMEOUT' | 'EDGE_ORIGIN_UNAVAILABLE' | 'EDGE_ERROR_PAGE';
|
|
21
|
+
export interface EdgeErrorPage {
|
|
22
|
+
status: number;
|
|
23
|
+
code: EdgeErrorCode;
|
|
24
|
+
/** The edge's own id for the request (CloudFront prints one), when the page carries it. */
|
|
25
|
+
requestId?: string;
|
|
26
|
+
message: string;
|
|
27
|
+
/** Whether sending the identical request again can help. */
|
|
28
|
+
retryable: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* True when an error response body is an HTML page rather than an API answer.
|
|
32
|
+
*
|
|
33
|
+
* A JSON body is never a page, whatever its text says: an API refusal that
|
|
34
|
+
* quotes "The request could not be satisfied" or an HTML fragment in its message
|
|
35
|
+
* must keep its own words (the base command's handler already gates on "no JSON
|
|
36
|
+
* body"; this makes every caller agree). The CloudFront wording counts only
|
|
37
|
+
* inside markup, never on its own.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isHtmlErrorPage(body: string | null | undefined): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Describe an HTML error page, or return null when the body is not one (a JSON
|
|
42
|
+
* refusal from the API must keep its own words).
|
|
43
|
+
*/
|
|
44
|
+
export declare function describeEdgeErrorPage(input: {
|
|
45
|
+
status: number | undefined;
|
|
46
|
+
body: string | null | undefined;
|
|
47
|
+
method?: string | null;
|
|
48
|
+
host?: string | null;
|
|
49
|
+
}): EdgeErrorPage | null;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* edge-error-page.ts — say what an HTML error page from the EDGE means, instead
|
|
4
|
+
* of printing it.
|
|
5
|
+
*
|
|
6
|
+
* Every API route answers in JSON. An HTML page in an error response therefore
|
|
7
|
+
* did not come from the API: it is CloudFront, its web application firewall, or
|
|
8
|
+
* a load balancer answering in the API's place. Printed as-is it is a screen of
|
|
9
|
+
* markup whose one useful line ("Request blocked", a request id) is buried, and
|
|
10
|
+
* treated as an API answer it is misleading in a specific way — a 403 reads as
|
|
11
|
+
* "you lack permission", and CloudFront's 504 page tells the reader to "try again
|
|
12
|
+
* later", which for a write that is still running repeats it.
|
|
13
|
+
*
|
|
14
|
+
* Both happened to `skrr machines dedicated exec` (OSK-8700, OSK-8705): a shell
|
|
15
|
+
* command containing `http://10.0.0.10/` came back as CloudFront's raw
|
|
16
|
+
* "403 ERROR / Request blocked" page, and a command running past 60 seconds as
|
|
17
|
+
* its raw 504 page while it kept running on the machine.
|
|
18
|
+
*
|
|
19
|
+
* General on purpose: nothing here knows which command asked.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.isHtmlErrorPage = isHtmlErrorPage;
|
|
23
|
+
exports.describeEdgeErrorPage = describeEdgeErrorPage;
|
|
24
|
+
const HTML_START = /^\s*(<!doctype html\b|<html\b)/i;
|
|
25
|
+
const EDGE_MARK = /Generated by cloudfront|The request could not be satisfied/i;
|
|
26
|
+
const REQUEST_ID = /Request ID:\s*([A-Za-z0-9_=-]{8,128})/i;
|
|
27
|
+
const MARKUP = /<(html|head|body|title|h1|pre)\b/i;
|
|
28
|
+
function isJson(body) {
|
|
29
|
+
const trimmed = body.trim();
|
|
30
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('['))
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
JSON.parse(trimmed);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* True when an error response body is an HTML page rather than an API answer.
|
|
42
|
+
*
|
|
43
|
+
* A JSON body is never a page, whatever its text says: an API refusal that
|
|
44
|
+
* quotes "The request could not be satisfied" or an HTML fragment in its message
|
|
45
|
+
* must keep its own words (the base command's handler already gates on "no JSON
|
|
46
|
+
* body"; this makes every caller agree). The CloudFront wording counts only
|
|
47
|
+
* inside markup, never on its own.
|
|
48
|
+
*/
|
|
49
|
+
function isHtmlErrorPage(body) {
|
|
50
|
+
if (typeof body !== 'string' || !body || isJson(body))
|
|
51
|
+
return false;
|
|
52
|
+
return HTML_START.test(body) || (MARKUP.test(body) && EDGE_MARK.test(body));
|
|
53
|
+
}
|
|
54
|
+
function readOnly(method) {
|
|
55
|
+
const upper = (method || 'GET').toUpperCase();
|
|
56
|
+
return upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS';
|
|
57
|
+
}
|
|
58
|
+
function where(host) {
|
|
59
|
+
return host ? ` (${host})` : '';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Describe an HTML error page, or return null when the body is not one (a JSON
|
|
63
|
+
* refusal from the API must keep its own words).
|
|
64
|
+
*/
|
|
65
|
+
function describeEdgeErrorPage(input) {
|
|
66
|
+
const { body, method, host } = input;
|
|
67
|
+
const status = Number(input.status) || 0;
|
|
68
|
+
if (!isHtmlErrorPage(body))
|
|
69
|
+
return null;
|
|
70
|
+
const requestId = REQUEST_ID.exec(String(body))?.[1];
|
|
71
|
+
const quote = requestId ? ` Edge request id: ${requestId} — quote it when reporting this.` : '';
|
|
72
|
+
const writeOutcome = readOnly(method)
|
|
73
|
+
? 'This request changed nothing; retry shortly.'
|
|
74
|
+
: 'For a write, the request may still have been carried out, or may still be running — check before sending it again.';
|
|
75
|
+
if (status === 403) {
|
|
76
|
+
return {
|
|
77
|
+
status,
|
|
78
|
+
code: 'EDGE_REQUEST_BLOCKED',
|
|
79
|
+
...(requestId ? { requestId } : {}),
|
|
80
|
+
message: `The request was blocked at the edge before it reached the API${where(host)}: a web ` +
|
|
81
|
+
'application firewall rule matched something in it. This is not a problem with your ' +
|
|
82
|
+
'account or permissions. Content that resembles an attack can be enough — a URL with an ' +
|
|
83
|
+
'IP address, a relative path (../), markup or SQL, even inside a command or a document — ' +
|
|
84
|
+
'and sending the same request again will be blocked again.' +
|
|
85
|
+
quote,
|
|
86
|
+
retryable: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (status === 504) {
|
|
90
|
+
return {
|
|
91
|
+
status,
|
|
92
|
+
code: 'EDGE_GATEWAY_TIMEOUT',
|
|
93
|
+
...(requestId ? { requestId } : {}),
|
|
94
|
+
message: `The edge stopped waiting for the API${where(host)} and answered HTTP 504. ` +
|
|
95
|
+
writeOutcome +
|
|
96
|
+
quote,
|
|
97
|
+
retryable: readOnly(method),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (status === 502 || status === 503) {
|
|
101
|
+
return {
|
|
102
|
+
status,
|
|
103
|
+
code: 'EDGE_ORIGIN_UNAVAILABLE',
|
|
104
|
+
...(requestId ? { requestId } : {}),
|
|
105
|
+
message: `The edge could not get an answer from the API${where(host)} (HTTP ${status}). ` +
|
|
106
|
+
writeOutcome +
|
|
107
|
+
quote,
|
|
108
|
+
retryable: readOnly(method),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
status,
|
|
113
|
+
code: 'EDGE_ERROR_PAGE',
|
|
114
|
+
...(requestId ? { requestId } : {}),
|
|
115
|
+
message: `The request did not get an API response: something in front of the API${where(host)} ` +
|
|
116
|
+
`answered HTTP ${status || 'error'} with an HTML page.` +
|
|
117
|
+
quote,
|
|
118
|
+
retryable: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
package/dist/lib/harnesses.d.ts
CHANGED
|
@@ -111,7 +111,15 @@ export interface HarnessLease {
|
|
|
111
111
|
createdAt?: string;
|
|
112
112
|
updatedAt?: string;
|
|
113
113
|
}
|
|
114
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Every action `POST /api/machines/leases/:id/actions/:action` accepts —
|
|
116
|
+
* `MACHINE_LEASE_ACTIONS` in MachineLeaseService. Which of them a given lease
|
|
117
|
+
* supports is its product's `lifecycleCapabilities`, decided by the server.
|
|
118
|
+
*
|
|
119
|
+
* `start`, `stop`, `restart` and `snapshot` were missing, so this command
|
|
120
|
+
* refused every Dedicated Runtime lifecycle verb at argument parsing — before
|
|
121
|
+
* the server that accepts them was asked — while claiming to mirror its set.
|
|
122
|
+
*/
|
|
115
123
|
export declare const HARNESS_LEASE_ACTIONS: string[];
|
|
116
124
|
/** `user` (the default) or `workspace`. */
|
|
117
125
|
export type HarnessDefaultScope = 'user' | 'workspace';
|
package/dist/lib/harnesses.js
CHANGED
|
@@ -23,8 +23,26 @@ exports.releaseHarnessLease = releaseHarnessLease;
|
|
|
23
23
|
exports.performHarnessLeaseAction = performHarnessLeaseAction;
|
|
24
24
|
exports.setHarnessLeaseBilling = setHarnessLeaseBilling;
|
|
25
25
|
const api_fetch_1 = require("./api-fetch");
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Every action `POST /api/machines/leases/:id/actions/:action` accepts —
|
|
28
|
+
* `MACHINE_LEASE_ACTIONS` in MachineLeaseService. Which of them a given lease
|
|
29
|
+
* supports is its product's `lifecycleCapabilities`, decided by the server.
|
|
30
|
+
*
|
|
31
|
+
* `start`, `stop`, `restart` and `snapshot` were missing, so this command
|
|
32
|
+
* refused every Dedicated Runtime lifecycle verb at argument parsing — before
|
|
33
|
+
* the server that accepts them was asked — while claiming to mirror its set.
|
|
34
|
+
*/
|
|
35
|
+
exports.HARNESS_LEASE_ACTIONS = [
|
|
36
|
+
'pause',
|
|
37
|
+
'resume',
|
|
38
|
+
'start',
|
|
39
|
+
'stop',
|
|
40
|
+
'restart',
|
|
41
|
+
'snapshot',
|
|
42
|
+
'destroy',
|
|
43
|
+
'terminate',
|
|
44
|
+
'health-check',
|
|
45
|
+
];
|
|
28
46
|
// ── Fleet reads ────────────────────────────────────────────────────────────
|
|
29
47
|
async function listHarnesses(filters = {}) {
|
|
30
48
|
const params = new URLSearchParams();
|
|
@@ -25,6 +25,7 @@ exports.execHostedMachine = execHostedMachine;
|
|
|
25
25
|
exports.waitForHostedMachineReady = waitForHostedMachineReady;
|
|
26
26
|
exports.offlineForLabel = offlineForLabel;
|
|
27
27
|
const api_fetch_1 = require("./api-fetch");
|
|
28
|
+
const edge_error_page_1 = require("./edge-error-page");
|
|
28
29
|
function curateHostedMachineList(response) {
|
|
29
30
|
return {
|
|
30
31
|
object: 'list',
|
|
@@ -148,6 +149,12 @@ function formatHostedMachineApiError(err) {
|
|
|
148
149
|
if (err.status === 401) {
|
|
149
150
|
return null;
|
|
150
151
|
}
|
|
152
|
+
// An HTML page is the edge answering in the API's place; describe it rather
|
|
153
|
+
// than printing its markup (OSK-8700).
|
|
154
|
+
const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
|
|
155
|
+
if (edge) {
|
|
156
|
+
return edge.message;
|
|
157
|
+
}
|
|
151
158
|
try {
|
|
152
159
|
const parsed = JSON.parse(err.body);
|
|
153
160
|
return parsed.message || parsed.error || parsed.code || null;
|
|
@@ -41331,6 +41331,18 @@ var nI = { get: function(e2, t2) {
|
|
|
41331
41331
|
}
|
|
41332
41332
|
});
|
|
41333
41333
|
});
|
|
41334
|
+
}, patchWithOptions: function(e2, t2, o2) {
|
|
41335
|
+
return v(this, void 0, void 0, function() {
|
|
41336
|
+
var r2;
|
|
41337
|
+
return h(this, function(a2) {
|
|
41338
|
+
switch (a2.label) {
|
|
41339
|
+
case 0:
|
|
41340
|
+
return r2 = zk("PATCH", e2, t2, null == o2 ? void 0 : o2.headers), Mk ? [2, Mk.patch(e2, t2, Ok(f(f({}, o2), { headers: r2 })))] : [4, n.patch(e2, JSON.stringify(t2), f(f({}, o2), { headers: f({ "Content-Type": "application/json" }, r2) }))];
|
|
41341
|
+
case 1:
|
|
41342
|
+
return [2, a2.sent().data];
|
|
41343
|
+
}
|
|
41344
|
+
});
|
|
41345
|
+
});
|
|
41334
41346
|
}, refreshToken: Zk, dispatchTokenUpdatedEvent: eI, broadcastLogout: $k, classifyRefreshFailure: Xk, noteRefreshSuccess: function() {
|
|
41335
41347
|
Yk();
|
|
41336
41348
|
} };
|