@skrr-ai/cli 0.1.26 → 0.1.28
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 +125 -6
- 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/lib/sky-code.js +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +12 -0
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/index.js +3 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/loopback-server.js +64 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/managed-inference-broker.d.ts +30 -0
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/managed-inference-broker.js +43 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/loopback-server.js +64 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/managed-inference-broker.d.ts +30 -0
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/managed-inference-broker.js +41 -0
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +2928 -2734
- 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,12 +297,30 @@ 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) => {
|
|
240
319
|
if (value === undefined || value === null || value === '')
|
|
241
320
|
return;
|
|
242
|
-
|
|
321
|
+
// At least one space: `restored from` is as wide as the column and printed
|
|
322
|
+
// as `restored fromrtl_…`.
|
|
323
|
+
lines.push(` ${`${label} `.padEnd(13)}${value}`);
|
|
243
324
|
};
|
|
244
325
|
row('id', dedicatedLeaseId(lease));
|
|
245
326
|
row('state', lease.stateReason
|
|
@@ -291,6 +372,11 @@ function describeDedicatedLease(lease, harnesses = []) {
|
|
|
291
372
|
.filter(Boolean)
|
|
292
373
|
.join(' · '));
|
|
293
374
|
}
|
|
375
|
+
const deletion = storage.deletion;
|
|
376
|
+
if (deletion?.status) {
|
|
377
|
+
row('deleted', describeDedicatedDeletion(deletion, dedicatedLeaseId(lease)));
|
|
378
|
+
}
|
|
379
|
+
row('restored from', lease.recovery?.restoredFromLeaseId);
|
|
294
380
|
const hold = lease.recovery?.incidentHold;
|
|
295
381
|
if (hold?.status === 'open') {
|
|
296
382
|
row('incident', `hold open since ${hold.heldAt || '?'} — the failed source volume is billed until \`incident-close\``);
|
|
@@ -328,6 +414,29 @@ function dedicatedLeaseHarnesses(lease, harnesses) {
|
|
|
328
414
|
})
|
|
329
415
|
.sort((a, b) => seenAt(b) - seenAt(a));
|
|
330
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* The lease of the machine this process is running on, from the harness rows.
|
|
419
|
+
*
|
|
420
|
+
* Nothing in a workload process's environment names its lease — the daemon's
|
|
421
|
+
* environment (lease, generation, credentials) sits on the other side of the uid
|
|
422
|
+
* split by design. The CLI hand-off descriptor the daemon publishes to the
|
|
423
|
+
* workload group does carry the daemon's id, and every harness row that daemon
|
|
424
|
+
* registered carries `metadata.machineLeaseId`. Joining the two answers "which
|
|
425
|
+
* machine am I" with no new daemon surface.
|
|
426
|
+
*/
|
|
427
|
+
function dedicatedLeaseIdForDaemon(daemonId, harnesses) {
|
|
428
|
+
if (!daemonId)
|
|
429
|
+
return null;
|
|
430
|
+
const leaseIds = new Set(harnesses
|
|
431
|
+
.filter((harness) => harness.daemonId === daemonId &&
|
|
432
|
+
harness.metadata?.machineProduct === exports.DEDICATED_MACHINE_PRODUCT &&
|
|
433
|
+
typeof harness.metadata?.machineLeaseId === 'string')
|
|
434
|
+
.map((harness) => String(harness.metadata?.machineLeaseId)));
|
|
435
|
+
// One daemon serves one lease. Two would mean rows from a replaced
|
|
436
|
+
// generation disagree, and guessing between them is how `self` would act on
|
|
437
|
+
// the wrong machine.
|
|
438
|
+
return leaseIds.size === 1 ? [...leaseIds][0] : null;
|
|
439
|
+
}
|
|
331
440
|
/**
|
|
332
441
|
* The row an interactive terminal opens on: online, local, with a daemon.
|
|
333
442
|
*
|
|
@@ -485,6 +594,10 @@ function dedicatedRuntimeApiErrorCode(err) {
|
|
|
485
594
|
if (!(err instanceof api_fetch_1.ApiFetchError)) {
|
|
486
595
|
return undefined;
|
|
487
596
|
}
|
|
597
|
+
const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
|
|
598
|
+
if (edge) {
|
|
599
|
+
return edge.code;
|
|
600
|
+
}
|
|
488
601
|
try {
|
|
489
602
|
const parsed = JSON.parse(err.body || '');
|
|
490
603
|
if (typeof parsed.code === 'string' && parsed.code) {
|
|
@@ -503,6 +616,12 @@ function formatDedicatedRuntimeApiError(err) {
|
|
|
503
616
|
if (err.status === 401) {
|
|
504
617
|
return null;
|
|
505
618
|
}
|
|
619
|
+
// An HTML page is the edge answering in the API's place — a WAF block or a
|
|
620
|
+
// gateway timeout. Printing its markup told nobody anything (OSK-8700).
|
|
621
|
+
const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
|
|
622
|
+
if (edge) {
|
|
623
|
+
return edge.message;
|
|
624
|
+
}
|
|
506
625
|
const notFound = describeLeaseNotFound(err);
|
|
507
626
|
if (notFound) {
|
|
508
627
|
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;
|
package/dist/lib/sky-code.js
CHANGED
|
@@ -36,6 +36,7 @@ const node_fs_1 = require("node:fs");
|
|
|
36
36
|
const node_os_1 = require("node:os");
|
|
37
37
|
const node_path_1 = __importDefault(require("node:path"));
|
|
38
38
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
39
|
+
const inference_broker_1 = require("@skrr-ai/inference-broker");
|
|
39
40
|
const config_1 = require("./config");
|
|
40
41
|
// Deliberately does NOT import the managed marker any more. This module used to
|
|
41
42
|
// read it to decide whether a run was managed; that decision now arrives as a
|
|
@@ -468,11 +469,20 @@ function engineSpawnEnv(env, extraEnv,
|
|
|
468
469
|
// Required, not defaulted: the safe value differs per call and a parameter that
|
|
469
470
|
// can be forgotten on a security boundary will be.
|
|
470
471
|
mode) {
|
|
471
|
-
|
|
472
|
+
const spawnEnv = {
|
|
472
473
|
...(0, auth_core_1.sanitizeSpawnEnv)(env),
|
|
473
474
|
...(mode === 'managed' ? {} : inheritedProviderCredentials(env)),
|
|
474
475
|
...extraEnv,
|
|
475
476
|
};
|
|
477
|
+
if (mode !== 'managed')
|
|
478
|
+
return spawnEnv;
|
|
479
|
+
// OSK-8758 — a managed engine reaches the broker at a synthetic host, and must
|
|
480
|
+
// never address that host to a proxy. Behind an egress proxy (a Dedicated
|
|
481
|
+
// Runtime guest) Bun wrote every broker request in proxy form, and a runtime
|
|
482
|
+
// that honoured the proxy over the socket would send the prompt to it. Layered
|
|
483
|
+
// over the env built above, so it extends whatever list the engine would
|
|
484
|
+
// otherwise have received rather than replacing it.
|
|
485
|
+
return { ...spawnEnv, ...(0, inference_broker_1.inferenceBrokerNoProxyEnv)(spawnEnv) };
|
|
476
486
|
}
|
|
477
487
|
async function execEngine(args, env = process.env, options = {}) {
|
|
478
488
|
// Resolve the binary FIRST, before `prepare` runs.
|
|
@@ -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
|
} };
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
* ESM and CJS are both emitted because the daemon is ESM and the CLI is CJS, and
|
|
52
52
|
* this package exists precisely so both can host the broker.
|
|
53
53
|
*/
|
|
54
|
-
export { INFERENCE_BROKER_ORIGIN, INFERENCE_BROKER_SOCKET_ENV, MANAGED_SESSION_ENV, MANAGED_PROMPT_CACHING_ENV, MANAGED_STATUS_PATH, type ManagedSessionStatus, buildManagedInferenceRelayCredential, startManagedInferenceBroker, isManagedInferenceModelScope, type GatewayCredential, type ManagedCapabilityRefresh, type ManagedCapabilityRefreshResult, type ManagedInferenceBrokerOptions, type ManagedInferenceBrokerSession, type ManagedInferenceInputTokenAccounting, type ManagedInferenceModelScope, type ManagedInferenceRequestEnvelope, type ManagedReasoningPolicy, } from './managed-inference-broker.js';
|
|
54
|
+
export { INFERENCE_BROKER_ORIGIN, INFERENCE_BROKER_SOCKET_ENV, MANAGED_SESSION_ENV, MANAGED_PROMPT_CACHING_ENV, MANAGED_STATUS_PATH, INFERENCE_BROKER_HOST, inferenceBrokerNoProxyEnv, type ManagedSessionStatus, buildManagedInferenceRelayCredential, startManagedInferenceBroker, isManagedInferenceModelScope, type GatewayCredential, type ManagedCapabilityRefresh, type ManagedCapabilityRefreshResult, type ManagedInferenceBrokerOptions, type ManagedInferenceBrokerSession, type ManagedInferenceInputTokenAccounting, type ManagedInferenceModelScope, type ManagedInferenceRequestEnvelope, type ManagedReasoningPolicy, } from './managed-inference-broker.js';
|
|
55
55
|
export { minimumCapabilityWindowMs, scheduleRenewalDelay, startRenewalLoop, type CapabilityRefreshOutcome, type RenewalLoop, type RenewalLoopOptions, type RenewalOutcome, type RenewalStopReason, } from './managed-inference-renewal.js';
|
|
56
56
|
export { ModelProxyLoopbackServer, type ModelProxyForwarder } from './loopback-server.js';
|
|
57
57
|
export * from './contract.js';
|
|
@@ -67,13 +67,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
67
67
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
68
68
|
};
|
|
69
69
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
70
|
-
exports.ModelProxyLoopbackServer = exports.startRenewalLoop = exports.scheduleRenewalDelay = exports.minimumCapabilityWindowMs = exports.isManagedInferenceModelScope = exports.startManagedInferenceBroker = exports.buildManagedInferenceRelayCredential = exports.MANAGED_STATUS_PATH = exports.MANAGED_PROMPT_CACHING_ENV = exports.MANAGED_SESSION_ENV = exports.INFERENCE_BROKER_SOCKET_ENV = exports.INFERENCE_BROKER_ORIGIN = void 0;
|
|
70
|
+
exports.ModelProxyLoopbackServer = exports.startRenewalLoop = exports.scheduleRenewalDelay = exports.minimumCapabilityWindowMs = exports.isManagedInferenceModelScope = exports.startManagedInferenceBroker = exports.buildManagedInferenceRelayCredential = exports.inferenceBrokerNoProxyEnv = exports.INFERENCE_BROKER_HOST = exports.MANAGED_STATUS_PATH = exports.MANAGED_PROMPT_CACHING_ENV = exports.MANAGED_SESSION_ENV = exports.INFERENCE_BROKER_SOCKET_ENV = exports.INFERENCE_BROKER_ORIGIN = void 0;
|
|
71
71
|
var managed_inference_broker_js_1 = require("./managed-inference-broker.js");
|
|
72
72
|
Object.defineProperty(exports, "INFERENCE_BROKER_ORIGIN", { enumerable: true, get: function () { return managed_inference_broker_js_1.INFERENCE_BROKER_ORIGIN; } });
|
|
73
73
|
Object.defineProperty(exports, "INFERENCE_BROKER_SOCKET_ENV", { enumerable: true, get: function () { return managed_inference_broker_js_1.INFERENCE_BROKER_SOCKET_ENV; } });
|
|
74
74
|
Object.defineProperty(exports, "MANAGED_SESSION_ENV", { enumerable: true, get: function () { return managed_inference_broker_js_1.MANAGED_SESSION_ENV; } });
|
|
75
75
|
Object.defineProperty(exports, "MANAGED_PROMPT_CACHING_ENV", { enumerable: true, get: function () { return managed_inference_broker_js_1.MANAGED_PROMPT_CACHING_ENV; } });
|
|
76
76
|
Object.defineProperty(exports, "MANAGED_STATUS_PATH", { enumerable: true, get: function () { return managed_inference_broker_js_1.MANAGED_STATUS_PATH; } });
|
|
77
|
+
Object.defineProperty(exports, "INFERENCE_BROKER_HOST", { enumerable: true, get: function () { return managed_inference_broker_js_1.INFERENCE_BROKER_HOST; } });
|
|
78
|
+
Object.defineProperty(exports, "inferenceBrokerNoProxyEnv", { enumerable: true, get: function () { return managed_inference_broker_js_1.inferenceBrokerNoProxyEnv; } });
|
|
77
79
|
Object.defineProperty(exports, "buildManagedInferenceRelayCredential", { enumerable: true, get: function () { return managed_inference_broker_js_1.buildManagedInferenceRelayCredential; } });
|
|
78
80
|
Object.defineProperty(exports, "startManagedInferenceBroker", { enumerable: true, get: function () { return managed_inference_broker_js_1.startManagedInferenceBroker; } });
|
|
79
81
|
Object.defineProperty(exports, "isManagedInferenceModelScope", { enumerable: true, get: function () { return managed_inference_broker_js_1.isManagedInferenceModelScope; } });
|
|
@@ -131,6 +131,14 @@ class ModelProxyLoopbackServer {
|
|
|
131
131
|
writeError(res, 401, contract_js_1.MODEL_PROXY_ERROR.UNAUTHORIZED, 'Invalid relay token.');
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
|
+
// Judged after authentication, so an unauthenticated caller learns nothing
|
|
135
|
+
// about which request forms this relay accepts; and before the body is read,
|
|
136
|
+
// so a refused request is never buffered.
|
|
137
|
+
const path = originFormPath(req.url ?? '/', new URL(this.baseUrl()).origin);
|
|
138
|
+
if (path === null) {
|
|
139
|
+
req.resume();
|
|
140
|
+
throw new contract_js_1.ModelProxyError(contract_js_1.MODEL_PROXY_ERROR.REQUEST_REJECTED, 'model proxy loopback accepts only requests addressed to its own origin');
|
|
141
|
+
}
|
|
134
142
|
const body = await readBody(req, this.opts.maxBodyBytes);
|
|
135
143
|
const controller = new AbortController();
|
|
136
144
|
this.activeRequests.add(controller);
|
|
@@ -143,7 +151,7 @@ class ModelProxyLoopbackServer {
|
|
|
143
151
|
});
|
|
144
152
|
const headers = normalizeIncomingHeaders(req.headers);
|
|
145
153
|
try {
|
|
146
|
-
const upstream = await this.opts.forwarder.forward({ method: req.method ?? 'POST', path
|
|
154
|
+
const upstream = await this.opts.forwarder.forward({ method: req.method ?? 'POST', path, headers, body }, { signal: controller.signal });
|
|
147
155
|
res.writeHead(upstream.status, upstream.headers);
|
|
148
156
|
for await (const chunk of upstream.body) {
|
|
149
157
|
if (!res.write(chunk)) {
|
|
@@ -179,6 +187,61 @@ function constantTimeEqual(a, b) {
|
|
|
179
187
|
return false;
|
|
180
188
|
return (0, node_crypto_1.timingSafeEqual)(bufA, bufB);
|
|
181
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* The request-target as the ORIGIN-FORM path the forwarder contract carries, or
|
|
192
|
+
* null when the request was addressed somewhere else.
|
|
193
|
+
*
|
|
194
|
+
* HTTP/1.1 gives a POST two request-target forms. Origin-form (`/v1/messages`) is
|
|
195
|
+
* what a client sends an origin server; absolute-form
|
|
196
|
+
* (`http://host/v1/messages`) is what it sends a forward PROXY — and RFC 9112
|
|
197
|
+
* §3.2.2 still requires a server to accept it. Node passes either through as
|
|
198
|
+
* `req.url`, verbatim.
|
|
199
|
+
*
|
|
200
|
+
* OSK-8758 is why this matters here. A Dedicated Runtime guest exports
|
|
201
|
+
* HTTP(S)_PROXY for its egress proxy, and the broker's synthetic
|
|
202
|
+
* `oversky-inference-broker.invalid` is in no NO_PROXY. Bun keeps dialing the Unix
|
|
203
|
+
* socket named by `fetch({ unix })` but writes the request line in proxy form, so
|
|
204
|
+
* every managed turn reached the broker with an absolute URL for a "path", failed
|
|
205
|
+
* its exact route allowlist, and was refused as an unapproved upstream route —
|
|
206
|
+
* while a laptop, with no proxy configured, sent origin-form and worked.
|
|
207
|
+
*
|
|
208
|
+
* Accepted only when the target denotes EXACTLY the request origin-form would
|
|
209
|
+
* have, so this is a parsing fix and never a widening:
|
|
210
|
+
*
|
|
211
|
+
* - the origin must be this relay's own advertised origin. This loopback is not a
|
|
212
|
+
* forward proxy: a foreign origin is refused here, even one a downstream egress
|
|
213
|
+
* allowlist would accept — naming the upstream is not a way to reach it;
|
|
214
|
+
* - no userinfo, which a request-target may not carry;
|
|
215
|
+
* - the path and query must survive URL parsing byte-for-byte. The WHATWG parser
|
|
216
|
+
* resolves dot segments and rewrites `\` to `/`, and origin-form bytes reach the
|
|
217
|
+
* forwarder unnormalized, so a spelling the forwarder would refuse in one form
|
|
218
|
+
* cannot become an approved route in the other.
|
|
219
|
+
*
|
|
220
|
+
* Anything that is not absolute-form (origin-form and the odd `*`) is returned
|
|
221
|
+
* untouched, exactly as before, for the forwarder to judge.
|
|
222
|
+
*/
|
|
223
|
+
function originFormPath(rawTarget, ownOrigin) {
|
|
224
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(rawTarget))
|
|
225
|
+
return rawTarget;
|
|
226
|
+
let target;
|
|
227
|
+
try {
|
|
228
|
+
target = new URL(rawTarget);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
// `origin` is the string "null" for every non-special scheme, so two unrelated
|
|
234
|
+
// opaque origins would otherwise compare equal.
|
|
235
|
+
if (target.origin === 'null' || target.origin !== ownOrigin)
|
|
236
|
+
return null;
|
|
237
|
+
if (target.username || target.password)
|
|
238
|
+
return null;
|
|
239
|
+
const authorityEnd = rawTarget.indexOf('/', rawTarget.indexOf('://') + 3);
|
|
240
|
+
if (authorityEnd < 0)
|
|
241
|
+
return null;
|
|
242
|
+
const path = rawTarget.slice(authorityEnd);
|
|
243
|
+
return path === `${target.pathname}${target.search}` ? path : null;
|
|
244
|
+
}
|
|
182
245
|
function normalizeIncomingHeaders(headers) {
|
|
183
246
|
const out = {};
|
|
184
247
|
for (const [key, value] of Object.entries(headers)) {
|