incus-ts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/index.js +1885 -0
- package/dist/index.js.map +10 -0
- package/package.json +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1885 @@
|
|
|
1
|
+
// src/incus.ts
|
|
2
|
+
import { request as nodeHttpRequest } from "node:http";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { connect as nodeNetConnect } from "node:net";
|
|
6
|
+
|
|
7
|
+
class IncusApiError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
statusCode;
|
|
10
|
+
errorCode;
|
|
11
|
+
details;
|
|
12
|
+
constructor(message, status, options = {}) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "IncusApiError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.statusCode = options.statusCode;
|
|
17
|
+
this.errorCode = options.errorCode;
|
|
18
|
+
this.details = options.details;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
22
|
+
var WS_CONNECTING = 0;
|
|
23
|
+
var WS_OPEN = 1;
|
|
24
|
+
var WS_CLOSING = 2;
|
|
25
|
+
var WS_CLOSED = 3;
|
|
26
|
+
|
|
27
|
+
class UnixSocketWebSocket {
|
|
28
|
+
socket;
|
|
29
|
+
CONNECTING = WS_CONNECTING;
|
|
30
|
+
OPEN = WS_OPEN;
|
|
31
|
+
CLOSING = WS_CLOSING;
|
|
32
|
+
CLOSED = WS_CLOSED;
|
|
33
|
+
binaryType = "arraybuffer";
|
|
34
|
+
extensions = "";
|
|
35
|
+
protocol = "";
|
|
36
|
+
readyState = WS_CONNECTING;
|
|
37
|
+
bufferedAmount = 0;
|
|
38
|
+
events = new EventEmitter;
|
|
39
|
+
readBuffer = Buffer.alloc(0);
|
|
40
|
+
fragments = [];
|
|
41
|
+
fragmentOpcode = null;
|
|
42
|
+
closeInfo = null;
|
|
43
|
+
closeEmitted = false;
|
|
44
|
+
constructor(socket, initialData) {
|
|
45
|
+
this.socket = socket;
|
|
46
|
+
this.socket.on("data", (chunk) => {
|
|
47
|
+
this.ingestData(chunk);
|
|
48
|
+
});
|
|
49
|
+
this.socket.on("error", (error) => {
|
|
50
|
+
this.events.emit("error", error);
|
|
51
|
+
});
|
|
52
|
+
this.socket.on("close", () => {
|
|
53
|
+
this.readyState = WS_CLOSED;
|
|
54
|
+
if (!this.closeEmitted) {
|
|
55
|
+
this.closeEmitted = true;
|
|
56
|
+
const info = this.closeInfo ?? { code: 1006, reason: "" };
|
|
57
|
+
this.events.emit("close", info.code, info.reason);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
this.readyState = WS_OPEN;
|
|
61
|
+
if (initialData && initialData.byteLength > 0) {
|
|
62
|
+
this.ingestData(Buffer.from(initialData));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
addEventListener(type, listener) {
|
|
66
|
+
this.events.on(type, listener);
|
|
67
|
+
}
|
|
68
|
+
removeEventListener(type, listener) {
|
|
69
|
+
this.events.off(type, listener);
|
|
70
|
+
}
|
|
71
|
+
on(type, listener) {
|
|
72
|
+
this.events.on(type, listener);
|
|
73
|
+
}
|
|
74
|
+
off(type, listener) {
|
|
75
|
+
this.events.off(type, listener);
|
|
76
|
+
}
|
|
77
|
+
once(type, listener) {
|
|
78
|
+
this.events.once(type, listener);
|
|
79
|
+
}
|
|
80
|
+
send(data) {
|
|
81
|
+
if (this.readyState !== WS_OPEN) {
|
|
82
|
+
throw new Error("[Incus.ts] Cannot send on a closed websocket");
|
|
83
|
+
}
|
|
84
|
+
const payload = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(toBytes(data));
|
|
85
|
+
const opcode = typeof data === "string" ? 1 : 2;
|
|
86
|
+
this.writeFrame(opcode, payload, true);
|
|
87
|
+
}
|
|
88
|
+
close(code = 1000, reason = "") {
|
|
89
|
+
if (this.readyState === WS_CLOSED) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (this.readyState === WS_OPEN) {
|
|
93
|
+
this.readyState = WS_CLOSING;
|
|
94
|
+
const reasonBytes = Buffer.from(reason, "utf8");
|
|
95
|
+
const payload = Buffer.alloc(2 + reasonBytes.length);
|
|
96
|
+
payload.writeUInt16BE(code, 0);
|
|
97
|
+
reasonBytes.copy(payload, 2);
|
|
98
|
+
this.writeFrame(8, payload, true);
|
|
99
|
+
}
|
|
100
|
+
this.socket.end();
|
|
101
|
+
}
|
|
102
|
+
ingestData(chunk) {
|
|
103
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
104
|
+
if (bytes.byteLength === 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.readBuffer = this.readBuffer.byteLength === 0 ? Buffer.from(bytes) : Buffer.concat([this.readBuffer, bytes]);
|
|
108
|
+
this.parseFrames();
|
|
109
|
+
}
|
|
110
|
+
parseFrames() {
|
|
111
|
+
while (this.readBuffer.byteLength >= 2) {
|
|
112
|
+
const first = this.readBuffer[0];
|
|
113
|
+
const second = this.readBuffer[1];
|
|
114
|
+
if (first === undefined || second === undefined) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const fin = (first & 128) !== 0;
|
|
118
|
+
let opcode = first & 15;
|
|
119
|
+
const masked = (second & 128) !== 0;
|
|
120
|
+
let offset = 2;
|
|
121
|
+
let payloadLength = second & 127;
|
|
122
|
+
if (payloadLength === 126) {
|
|
123
|
+
if (this.readBuffer.byteLength < offset + 2) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
payloadLength = this.readBuffer.readUInt16BE(offset);
|
|
127
|
+
offset += 2;
|
|
128
|
+
} else if (payloadLength === 127) {
|
|
129
|
+
if (this.readBuffer.byteLength < offset + 8) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const extended = this.readBuffer.readBigUInt64BE(offset);
|
|
133
|
+
if (extended > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
134
|
+
this.events.emit("error", new Error("[Incus.ts] Websocket frame too large"));
|
|
135
|
+
this.close(1009, "Frame too large");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
payloadLength = Number(extended);
|
|
139
|
+
offset += 8;
|
|
140
|
+
}
|
|
141
|
+
let maskKey;
|
|
142
|
+
if (masked) {
|
|
143
|
+
if (this.readBuffer.byteLength < offset + 4) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
maskKey = this.readBuffer.subarray(offset, offset + 4);
|
|
147
|
+
offset += 4;
|
|
148
|
+
}
|
|
149
|
+
if (this.readBuffer.byteLength < offset + payloadLength) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let payload = this.readBuffer.subarray(offset, offset + payloadLength);
|
|
153
|
+
this.readBuffer = this.readBuffer.subarray(offset + payloadLength);
|
|
154
|
+
if (maskKey) {
|
|
155
|
+
const decoded = Buffer.alloc(payload.byteLength);
|
|
156
|
+
for (let i = 0;i < payload.byteLength; i += 1) {
|
|
157
|
+
decoded[i] = payload[i] ^ maskKey[i % 4];
|
|
158
|
+
}
|
|
159
|
+
payload = decoded;
|
|
160
|
+
}
|
|
161
|
+
if (!fin) {
|
|
162
|
+
if (opcode !== 0) {
|
|
163
|
+
this.fragmentOpcode = opcode;
|
|
164
|
+
}
|
|
165
|
+
this.fragments.push(Buffer.from(payload));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (opcode === 0 && this.fragmentOpcode !== null) {
|
|
169
|
+
this.fragments.push(Buffer.from(payload));
|
|
170
|
+
payload = Buffer.concat(this.fragments);
|
|
171
|
+
opcode = this.fragmentOpcode;
|
|
172
|
+
this.fragments = [];
|
|
173
|
+
this.fragmentOpcode = null;
|
|
174
|
+
}
|
|
175
|
+
this.handleFrame(opcode, payload);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
handleFrame(opcode, payload) {
|
|
179
|
+
switch (opcode) {
|
|
180
|
+
case 1:
|
|
181
|
+
this.events.emit("message", payload.toString("utf8"));
|
|
182
|
+
break;
|
|
183
|
+
case 2:
|
|
184
|
+
this.events.emit("message", new Uint8Array(payload));
|
|
185
|
+
break;
|
|
186
|
+
case 8: {
|
|
187
|
+
let code = 1000;
|
|
188
|
+
let reason = "";
|
|
189
|
+
if (payload.byteLength >= 2) {
|
|
190
|
+
code = payload.readUInt16BE(0);
|
|
191
|
+
reason = payload.subarray(2).toString("utf8");
|
|
192
|
+
}
|
|
193
|
+
this.closeInfo = { code, reason };
|
|
194
|
+
if (this.readyState === WS_OPEN) {
|
|
195
|
+
this.readyState = WS_CLOSING;
|
|
196
|
+
this.writeFrame(8, payload, true);
|
|
197
|
+
}
|
|
198
|
+
this.socket.end();
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case 9:
|
|
202
|
+
this.writeFrame(10, payload, true);
|
|
203
|
+
break;
|
|
204
|
+
case 10:
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
this.events.emit("error", new Error(`[Incus.ts] Unsupported websocket opcode: ${opcode}`));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
writeFrame(opcode, payload, mask) {
|
|
211
|
+
const parts = [];
|
|
212
|
+
const firstByte = 128 | opcode & 15;
|
|
213
|
+
const payloadLength = payload.byteLength;
|
|
214
|
+
if (payloadLength < 126) {
|
|
215
|
+
parts.push(Buffer.from([firstByte, (mask ? 128 : 0) | payloadLength]));
|
|
216
|
+
} else if (payloadLength <= 65535) {
|
|
217
|
+
const header = Buffer.alloc(4);
|
|
218
|
+
header[0] = firstByte;
|
|
219
|
+
header[1] = (mask ? 128 : 0) | 126;
|
|
220
|
+
header.writeUInt16BE(payloadLength, 2);
|
|
221
|
+
parts.push(header);
|
|
222
|
+
} else {
|
|
223
|
+
const header = Buffer.alloc(10);
|
|
224
|
+
header[0] = firstByte;
|
|
225
|
+
header[1] = (mask ? 128 : 0) | 127;
|
|
226
|
+
header.writeBigUInt64BE(BigInt(payloadLength), 2);
|
|
227
|
+
parts.push(header);
|
|
228
|
+
}
|
|
229
|
+
if (mask) {
|
|
230
|
+
const maskKey = randomBytes(4);
|
|
231
|
+
const maskedPayload = Buffer.from(payload);
|
|
232
|
+
for (let i = 0;i < maskedPayload.byteLength; i += 1) {
|
|
233
|
+
maskedPayload[i] = maskedPayload[i] ^ maskKey[i % 4];
|
|
234
|
+
}
|
|
235
|
+
parts.push(maskKey, maskedPayload);
|
|
236
|
+
} else {
|
|
237
|
+
parts.push(payload);
|
|
238
|
+
}
|
|
239
|
+
this.socket.write(Buffer.concat(parts));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
class FetchTransport {
|
|
244
|
+
descriptor;
|
|
245
|
+
defaults;
|
|
246
|
+
constructor(descriptor, defaults) {
|
|
247
|
+
this.descriptor = descriptor;
|
|
248
|
+
this.defaults = defaults;
|
|
249
|
+
}
|
|
250
|
+
async request(method, path, options = {}) {
|
|
251
|
+
const url = this.makeRequestUrl(path, options.query);
|
|
252
|
+
const headers = this.makeHeaders(options);
|
|
253
|
+
const bodyInfo = prepareBody(options.body);
|
|
254
|
+
if (bodyInfo.contentType && !headers.has("Content-Type")) {
|
|
255
|
+
headers.set("Content-Type", bodyInfo.contentType);
|
|
256
|
+
}
|
|
257
|
+
const fetchImpl = this.defaults.fetch ?? fetch;
|
|
258
|
+
const response = await fetchImpl(url, {
|
|
259
|
+
method: method.toUpperCase(),
|
|
260
|
+
headers,
|
|
261
|
+
body: bodyInfo.body,
|
|
262
|
+
signal: options.signal ?? this.defaults.signal,
|
|
263
|
+
redirect: "follow"
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
status: response.status,
|
|
267
|
+
etag: response.headers.get("etag") ?? undefined,
|
|
268
|
+
headers: response.headers,
|
|
269
|
+
data: await parseResponseBody(response)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async websocket(path, protocols) {
|
|
273
|
+
const baseUrl = toWsBaseURL(this.descriptor.endpoint);
|
|
274
|
+
const target = path.startsWith("ws://") || path.startsWith("wss://") ? path : new URL(normalizePath(path), baseUrl).toString();
|
|
275
|
+
return new WebSocket(target, protocols);
|
|
276
|
+
}
|
|
277
|
+
makeRequestUrl(path, query) {
|
|
278
|
+
const isAbsolute = /^https?:\/\//.test(path);
|
|
279
|
+
const url = isAbsolute ? new URL(path) : new URL(normalizePath(path), `${this.descriptor.endpoint}/`);
|
|
280
|
+
if (query) {
|
|
281
|
+
for (const [key, rawValue] of Object.entries(query)) {
|
|
282
|
+
if (rawValue === undefined) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
url.searchParams.set(key, String(rawValue));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return url.toString();
|
|
289
|
+
}
|
|
290
|
+
makeHeaders(options) {
|
|
291
|
+
const headers = new Headers(this.defaults.headers);
|
|
292
|
+
applyHeaders(headers, options.headers);
|
|
293
|
+
if (this.defaults.userAgent && !headers.has("User-Agent")) {
|
|
294
|
+
headers.set("User-Agent", this.defaults.userAgent);
|
|
295
|
+
}
|
|
296
|
+
if (options.etag) {
|
|
297
|
+
headers.set("If-Match", options.etag);
|
|
298
|
+
}
|
|
299
|
+
if (options.context?.requireAuthenticated) {
|
|
300
|
+
headers.set("X-Incus-authenticated", "true");
|
|
301
|
+
}
|
|
302
|
+
if (this.defaults.oidcTokens?.accessToken && !headers.has("Authorization")) {
|
|
303
|
+
headers.set("Authorization", `Bearer ${this.defaults.oidcTokens.accessToken}`);
|
|
304
|
+
}
|
|
305
|
+
return headers;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
class UnixSocketTransport {
|
|
310
|
+
descriptor;
|
|
311
|
+
defaults;
|
|
312
|
+
constructor(descriptor, defaults) {
|
|
313
|
+
this.descriptor = descriptor;
|
|
314
|
+
this.defaults = defaults;
|
|
315
|
+
}
|
|
316
|
+
async request(method, path, options = {}) {
|
|
317
|
+
const socketPath = this.descriptor.socketPath;
|
|
318
|
+
if (!socketPath) {
|
|
319
|
+
throw new Error("[Incus.ts] Missing Unix socket path");
|
|
320
|
+
}
|
|
321
|
+
const requestPath = this.makeRequestPath(path, options.query);
|
|
322
|
+
const headers = this.makeHeaders(options);
|
|
323
|
+
const bodyInfo = await prepareUnixBody(options.body);
|
|
324
|
+
if (bodyInfo.contentType && !headers.has("Content-Type")) {
|
|
325
|
+
headers.set("Content-Type", bodyInfo.contentType);
|
|
326
|
+
}
|
|
327
|
+
if (bodyInfo.bodyBytes) {
|
|
328
|
+
headers.set("Content-Length", String(bodyInfo.bodyBytes.byteLength));
|
|
329
|
+
}
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
const req = nodeHttpRequest({
|
|
332
|
+
socketPath,
|
|
333
|
+
path: requestPath,
|
|
334
|
+
method: method.toUpperCase(),
|
|
335
|
+
headers: headersToObject(headers)
|
|
336
|
+
}, (res) => {
|
|
337
|
+
const chunks = [];
|
|
338
|
+
res.on("data", (chunk) => {
|
|
339
|
+
if (typeof chunk === "string") {
|
|
340
|
+
chunks.push(Buffer.from(chunk));
|
|
341
|
+
} else {
|
|
342
|
+
chunks.push(chunk);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
res.on("end", () => {
|
|
346
|
+
const bytes = Buffer.concat(chunks);
|
|
347
|
+
const responseHeaders = nodeHeadersToWebHeaders(res.headers);
|
|
348
|
+
const contentType = responseHeaders.get("content-type") ?? undefined;
|
|
349
|
+
const parsed = parseRawBody(contentType, bytes);
|
|
350
|
+
resolve({
|
|
351
|
+
status: res.statusCode ?? 0,
|
|
352
|
+
etag: responseHeaders.get("etag") ?? undefined,
|
|
353
|
+
headers: responseHeaders,
|
|
354
|
+
data: parsed
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
req.on("error", reject);
|
|
359
|
+
if (options.signal) {
|
|
360
|
+
options.signal.addEventListener("abort", () => {
|
|
361
|
+
req.destroy(new Error("Request aborted"));
|
|
362
|
+
}, { once: true });
|
|
363
|
+
}
|
|
364
|
+
if (bodyInfo.bodyBytes) {
|
|
365
|
+
req.write(bodyInfo.bodyBytes);
|
|
366
|
+
}
|
|
367
|
+
req.end();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async websocket(path, protocols) {
|
|
371
|
+
const socketPath = this.descriptor.socketPath;
|
|
372
|
+
if (!socketPath) {
|
|
373
|
+
throw new Error("[Incus.ts] Missing Unix socket path");
|
|
374
|
+
}
|
|
375
|
+
const requestPath = path.startsWith("ws://") || path.startsWith("wss://") ? (() => {
|
|
376
|
+
const url = new URL(path);
|
|
377
|
+
return `${url.pathname}${url.search}`;
|
|
378
|
+
})() : normalizePath(path);
|
|
379
|
+
const headers = this.makeWebsocketHeaders();
|
|
380
|
+
if (!headers.has("Host")) {
|
|
381
|
+
headers.set("Host", "incus.local");
|
|
382
|
+
}
|
|
383
|
+
const websocketKey = randomBytes(16).toString("base64");
|
|
384
|
+
headers.set("Connection", "Upgrade");
|
|
385
|
+
headers.set("Upgrade", "websocket");
|
|
386
|
+
headers.set("Sec-WebSocket-Version", "13");
|
|
387
|
+
headers.set("Sec-WebSocket-Key", websocketKey);
|
|
388
|
+
if (protocols) {
|
|
389
|
+
const selected = Array.isArray(protocols) ? protocols.join(", ") : protocols;
|
|
390
|
+
if (selected.length > 0) {
|
|
391
|
+
headers.set("Sec-WebSocket-Protocol", selected);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const requestLines = [`GET ${requestPath} HTTP/1.1`];
|
|
395
|
+
for (const [key, value] of headers.entries()) {
|
|
396
|
+
requestLines.push(`${key}: ${value}`);
|
|
397
|
+
}
|
|
398
|
+
requestLines.push("", "");
|
|
399
|
+
return new Promise((resolve, reject) => {
|
|
400
|
+
const socket = nodeNetConnect(socketPath);
|
|
401
|
+
let handshakeBuffer = Buffer.alloc(0);
|
|
402
|
+
let settled = false;
|
|
403
|
+
const finish = (handler, tail) => {
|
|
404
|
+
if (settled) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
settled = true;
|
|
408
|
+
cleanup();
|
|
409
|
+
handler(socket, tail);
|
|
410
|
+
};
|
|
411
|
+
const fail = (error) => {
|
|
412
|
+
if (settled) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
settled = true;
|
|
416
|
+
cleanup();
|
|
417
|
+
socket.destroy();
|
|
418
|
+
reject(error);
|
|
419
|
+
};
|
|
420
|
+
const timeout = setTimeout(() => {
|
|
421
|
+
fail(new Error("[Incus.ts] WebSocket opening handshake timed out"));
|
|
422
|
+
}, 1e4);
|
|
423
|
+
const cleanup = () => {
|
|
424
|
+
clearTimeout(timeout);
|
|
425
|
+
socket.off("connect", onConnect);
|
|
426
|
+
socket.off("data", onData);
|
|
427
|
+
socket.off("error", onError);
|
|
428
|
+
socket.off("close", onClose);
|
|
429
|
+
};
|
|
430
|
+
const onConnect = () => {
|
|
431
|
+
socket.write(requestLines.join(`\r
|
|
432
|
+
`));
|
|
433
|
+
};
|
|
434
|
+
const onData = (chunk) => {
|
|
435
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
436
|
+
handshakeBuffer = handshakeBuffer.byteLength === 0 ? Buffer.from(bytes) : Buffer.concat([handshakeBuffer, bytes]);
|
|
437
|
+
const delimiterIndex = handshakeBuffer.indexOf(`\r
|
|
438
|
+
\r
|
|
439
|
+
`);
|
|
440
|
+
if (delimiterIndex < 0) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const responseHeaders = handshakeBuffer.subarray(0, delimiterIndex + 4);
|
|
444
|
+
const leftover = handshakeBuffer.subarray(delimiterIndex + 4);
|
|
445
|
+
const responseText = responseHeaders.toString("utf8");
|
|
446
|
+
const lines = responseText.split(`\r
|
|
447
|
+
`);
|
|
448
|
+
const statusLine = lines.shift() ?? "";
|
|
449
|
+
const statusMatch = /^HTTP\/1\.[01]\s+(\d+)/.exec(statusLine);
|
|
450
|
+
const status = statusMatch ? Number.parseInt(statusMatch[1], 10) : 0;
|
|
451
|
+
if (status !== 101) {
|
|
452
|
+
fail(new Error(`[Incus.ts] WebSocket upgrade failed with status ${status || "unknown"}`));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const parsedHeaders = new Map;
|
|
456
|
+
for (const line of lines) {
|
|
457
|
+
if (line.length === 0) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const separator = line.indexOf(":");
|
|
461
|
+
if (separator < 0) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const key = line.slice(0, separator).trim().toLowerCase();
|
|
465
|
+
const value = line.slice(separator + 1).trim();
|
|
466
|
+
parsedHeaders.set(key, value);
|
|
467
|
+
}
|
|
468
|
+
const upgradeHeader = parsedHeaders.get("upgrade")?.toLowerCase();
|
|
469
|
+
if (upgradeHeader !== "websocket") {
|
|
470
|
+
fail(new Error("[Incus.ts] Invalid websocket upgrade response"));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const accept = parsedHeaders.get("sec-websocket-accept");
|
|
474
|
+
const expectedAccept = createHash("sha1").update(`${websocketKey}${WEBSOCKET_GUID}`).digest("base64");
|
|
475
|
+
if (accept !== expectedAccept) {
|
|
476
|
+
fail(new Error("[Incus.ts] Invalid websocket accept token"));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
finish((upgradedSocket, tail) => {
|
|
480
|
+
const websocket = new UnixSocketWebSocket(upgradedSocket, tail.byteLength > 0 ? tail : undefined);
|
|
481
|
+
resolve(websocket);
|
|
482
|
+
}, new Uint8Array(leftover));
|
|
483
|
+
};
|
|
484
|
+
const onError = (error) => {
|
|
485
|
+
fail(error);
|
|
486
|
+
};
|
|
487
|
+
const onClose = () => {
|
|
488
|
+
fail(new Error("[Incus.ts] WebSocket connection closed before upgrade completed"));
|
|
489
|
+
};
|
|
490
|
+
socket.on("connect", onConnect);
|
|
491
|
+
socket.on("data", onData);
|
|
492
|
+
socket.on("error", onError);
|
|
493
|
+
socket.on("close", onClose);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
makeRequestPath(path, query) {
|
|
497
|
+
const url = new URL(normalizePath(path), "http://incus.local");
|
|
498
|
+
if (query) {
|
|
499
|
+
for (const [key, rawValue] of Object.entries(query)) {
|
|
500
|
+
if (rawValue === undefined) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
url.searchParams.set(key, String(rawValue));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return `${url.pathname}${url.search}`;
|
|
507
|
+
}
|
|
508
|
+
makeHeaders(options) {
|
|
509
|
+
const headers = new Headers(this.defaults.headers);
|
|
510
|
+
applyHeaders(headers, options.headers);
|
|
511
|
+
if (this.defaults.userAgent && !headers.has("User-Agent")) {
|
|
512
|
+
headers.set("User-Agent", this.defaults.userAgent);
|
|
513
|
+
}
|
|
514
|
+
if (options.etag) {
|
|
515
|
+
headers.set("If-Match", options.etag);
|
|
516
|
+
}
|
|
517
|
+
if (options.context?.requireAuthenticated) {
|
|
518
|
+
headers.set("X-Incus-authenticated", "true");
|
|
519
|
+
}
|
|
520
|
+
if (this.defaults.oidcTokens?.accessToken && !headers.has("Authorization")) {
|
|
521
|
+
headers.set("Authorization", `Bearer ${this.defaults.oidcTokens.accessToken}`);
|
|
522
|
+
}
|
|
523
|
+
return headers;
|
|
524
|
+
}
|
|
525
|
+
makeWebsocketHeaders() {
|
|
526
|
+
const headers = new Headers(this.defaults.headers);
|
|
527
|
+
if (this.defaults.userAgent && !headers.has("User-Agent")) {
|
|
528
|
+
headers.set("User-Agent", this.defaults.userAgent);
|
|
529
|
+
}
|
|
530
|
+
if (this.defaults.oidcTokens?.accessToken && !headers.has("Authorization")) {
|
|
531
|
+
headers.set("Authorization", `Bearer ${this.defaults.oidcTokens.accessToken}`);
|
|
532
|
+
}
|
|
533
|
+
return headers;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function normalizeEndpoint(endpoint) {
|
|
537
|
+
if (endpoint.length <= 1) {
|
|
538
|
+
return endpoint;
|
|
539
|
+
}
|
|
540
|
+
return endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
|
|
541
|
+
}
|
|
542
|
+
function normalizePath(path) {
|
|
543
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
544
|
+
return path;
|
|
545
|
+
}
|
|
546
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
547
|
+
}
|
|
548
|
+
function toWsBaseURL(endpoint) {
|
|
549
|
+
const url = new URL(endpoint);
|
|
550
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
551
|
+
if (!url.pathname.endsWith("/")) {
|
|
552
|
+
url.pathname = `${url.pathname}/`;
|
|
553
|
+
}
|
|
554
|
+
return url.toString();
|
|
555
|
+
}
|
|
556
|
+
function applyHeaders(target, source) {
|
|
557
|
+
if (!source) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const toApply = new Headers(source);
|
|
561
|
+
for (const [key, value] of toApply.entries()) {
|
|
562
|
+
target.set(key, value);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function headersToObject(headers) {
|
|
566
|
+
const result = {};
|
|
567
|
+
for (const [key, value] of headers.entries()) {
|
|
568
|
+
result[key] = value;
|
|
569
|
+
}
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
function nodeHeadersToWebHeaders(headers) {
|
|
573
|
+
const result = new Headers;
|
|
574
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
575
|
+
if (value === undefined) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (Array.isArray(value)) {
|
|
579
|
+
result.set(key, value.join(", "));
|
|
580
|
+
} else {
|
|
581
|
+
result.set(key, value);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
function isIncusEnvelope(value) {
|
|
587
|
+
if (!value || typeof value !== "object") {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
const asRecord = value;
|
|
591
|
+
return "type" in asRecord && "metadata" in asRecord;
|
|
592
|
+
}
|
|
593
|
+
async function parseResponseBody(response) {
|
|
594
|
+
if (response.status === 204) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
598
|
+
if (contentType.includes("application/json")) {
|
|
599
|
+
return response.json();
|
|
600
|
+
}
|
|
601
|
+
if (contentType.startsWith("text/") || contentType.includes("application/openmetrics-text")) {
|
|
602
|
+
return response.text();
|
|
603
|
+
}
|
|
604
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
605
|
+
}
|
|
606
|
+
function parseRawBody(contentType, bytes) {
|
|
607
|
+
if (bytes.byteLength === 0) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
const type = contentType ?? "";
|
|
611
|
+
if (type.includes("application/json")) {
|
|
612
|
+
try {
|
|
613
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
614
|
+
} catch {
|
|
615
|
+
return new TextDecoder().decode(bytes);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (type.startsWith("text/") || type.includes("application/openmetrics-text")) {
|
|
619
|
+
return new TextDecoder().decode(bytes);
|
|
620
|
+
}
|
|
621
|
+
return bytes;
|
|
622
|
+
}
|
|
623
|
+
function parseFilters(filters) {
|
|
624
|
+
const translated = [];
|
|
625
|
+
for (const filter of filters) {
|
|
626
|
+
if (!filter.includes("=")) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const [left, right] = filter.split("=", 2);
|
|
630
|
+
translated.push(`${left} eq ${right}`);
|
|
631
|
+
}
|
|
632
|
+
return translated.join(" and ");
|
|
633
|
+
}
|
|
634
|
+
function urlsToResourceNames(matchPathPrefix, urls) {
|
|
635
|
+
const normalizedPrefix = `${matchPathPrefix.replace(/\/$/, "")}/`;
|
|
636
|
+
return urls.map((rawUrl) => {
|
|
637
|
+
const parsed = new URL(rawUrl, "http://incus.local");
|
|
638
|
+
const index = parsed.pathname.indexOf(normalizedPrefix);
|
|
639
|
+
if (index < 0) {
|
|
640
|
+
throw new Error(`[Incus.ts] Unexpected resource URL: ${rawUrl}`);
|
|
641
|
+
}
|
|
642
|
+
return decodeURIComponent(parsed.pathname.slice(index + normalizedPrefix.length));
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function toStringArray(value) {
|
|
646
|
+
if (!Array.isArray(value)) {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
return value.filter((entry) => typeof entry === "string");
|
|
650
|
+
}
|
|
651
|
+
function toRecord(value) {
|
|
652
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
653
|
+
return {};
|
|
654
|
+
}
|
|
655
|
+
return value;
|
|
656
|
+
}
|
|
657
|
+
function toRecordArray(value) {
|
|
658
|
+
if (!Array.isArray(value)) {
|
|
659
|
+
return [];
|
|
660
|
+
}
|
|
661
|
+
return value.filter((entry) => !!entry && typeof entry === "object").map((entry) => entry);
|
|
662
|
+
}
|
|
663
|
+
function flattenOperationMap(value) {
|
|
664
|
+
if (Array.isArray(value)) {
|
|
665
|
+
return toRecordArray(value);
|
|
666
|
+
}
|
|
667
|
+
if (!value || typeof value !== "object") {
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
const out = [];
|
|
671
|
+
for (const entry of Object.values(value)) {
|
|
672
|
+
if (Array.isArray(entry)) {
|
|
673
|
+
out.push(...toRecordArray(entry));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
function decodeOperationId(operationValue) {
|
|
679
|
+
const cleaned = operationValue.split("?")[0] ?? operationValue;
|
|
680
|
+
const parts = cleaned.split("/").filter(Boolean);
|
|
681
|
+
return decodeURIComponent(parts[parts.length - 1] ?? operationValue);
|
|
682
|
+
}
|
|
683
|
+
function operationPath(id) {
|
|
684
|
+
return `/1.0/operations/${encodeURIComponent(id)}`;
|
|
685
|
+
}
|
|
686
|
+
function toReadableStream(data) {
|
|
687
|
+
return new ReadableStream({
|
|
688
|
+
start(controller) {
|
|
689
|
+
controller.enqueue(data);
|
|
690
|
+
controller.close();
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
function toBytes(data) {
|
|
695
|
+
if (data instanceof Uint8Array) {
|
|
696
|
+
return data;
|
|
697
|
+
}
|
|
698
|
+
if (typeof data === "string") {
|
|
699
|
+
return new TextEncoder().encode(data);
|
|
700
|
+
}
|
|
701
|
+
if (data === null || data === undefined) {
|
|
702
|
+
return new Uint8Array;
|
|
703
|
+
}
|
|
704
|
+
return new TextEncoder().encode(JSON.stringify(data));
|
|
705
|
+
}
|
|
706
|
+
function toStringRecord(value) {
|
|
707
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
708
|
+
return {};
|
|
709
|
+
}
|
|
710
|
+
const out = {};
|
|
711
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
712
|
+
if (typeof entry === "string") {
|
|
713
|
+
out[key] = entry;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return out;
|
|
717
|
+
}
|
|
718
|
+
function parseOperationIdFromEnvelope(result) {
|
|
719
|
+
if (typeof result.operation === "string" && result.operation.length > 0) {
|
|
720
|
+
return decodeOperationId(result.operation);
|
|
721
|
+
}
|
|
722
|
+
if (result.value && typeof result.value === "object") {
|
|
723
|
+
const asRecord = result.value;
|
|
724
|
+
if (typeof asRecord.id === "string" && asRecord.id.length > 0) {
|
|
725
|
+
return decodeOperationId(asRecord.id);
|
|
726
|
+
}
|
|
727
|
+
if (typeof asRecord.operation === "string" && asRecord.operation.length > 0) {
|
|
728
|
+
return decodeOperationId(asRecord.operation);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
function assertOperationSuccess(operation, source) {
|
|
734
|
+
const err = operation.err;
|
|
735
|
+
if (typeof err === "string" && err.trim().length > 0) {
|
|
736
|
+
throw new Error(`[Incus.ts] Operation ${source} failed: ${err}`);
|
|
737
|
+
}
|
|
738
|
+
const statusCode = operation.status_code;
|
|
739
|
+
if (typeof statusCode === "number" && statusCode >= 400) {
|
|
740
|
+
const status = typeof operation.status === "string" ? ` (${operation.status})` : "";
|
|
741
|
+
throw new Error(`[Incus.ts] Operation ${source} failed with status code ${statusCode}${status}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function extractExecFds(operationValue) {
|
|
745
|
+
const record = toRecord(operationValue);
|
|
746
|
+
const candidates = [
|
|
747
|
+
record.fds,
|
|
748
|
+
toRecord(record.metadata).fds,
|
|
749
|
+
toRecord(toRecord(record.operation).metadata).fds,
|
|
750
|
+
toRecord(toRecord(record.metadata).operation).fds
|
|
751
|
+
];
|
|
752
|
+
for (const candidate of candidates) {
|
|
753
|
+
const fds = toStringRecord(candidate);
|
|
754
|
+
if (Object.keys(fds).length > 0) {
|
|
755
|
+
return fds;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return {};
|
|
759
|
+
}
|
|
760
|
+
function getBooleanValue(record, keys) {
|
|
761
|
+
for (const key of keys) {
|
|
762
|
+
const value = record[key];
|
|
763
|
+
if (typeof value === "boolean") {
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
function isInteractiveExecRequest(request) {
|
|
770
|
+
return getBooleanValue(request, ["interactive", "Interactive"]) ?? false;
|
|
771
|
+
}
|
|
772
|
+
function hasWaitForWebsocketFlag(request) {
|
|
773
|
+
return getBooleanValue(request, [
|
|
774
|
+
"wait-for-websocket",
|
|
775
|
+
"wait_for_websocket",
|
|
776
|
+
"waitForWebsocket",
|
|
777
|
+
"waitForWS",
|
|
778
|
+
"WaitForWS"
|
|
779
|
+
]) !== undefined;
|
|
780
|
+
}
|
|
781
|
+
function isNodeWebSocket(socket) {
|
|
782
|
+
return typeof socket.on === "function";
|
|
783
|
+
}
|
|
784
|
+
function addWebSocketMessageListener(socket, listener) {
|
|
785
|
+
if (isNodeWebSocket(socket) && socket.on && socket.off) {
|
|
786
|
+
const nodeListener = (data) => {
|
|
787
|
+
listener(data);
|
|
788
|
+
};
|
|
789
|
+
socket.on("message", nodeListener);
|
|
790
|
+
return () => {
|
|
791
|
+
socket.off?.("message", nodeListener);
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
const domListener = (event) => {
|
|
795
|
+
const data = event && typeof event === "object" && "data" in event ? event.data : event;
|
|
796
|
+
listener(data);
|
|
797
|
+
};
|
|
798
|
+
socket.addEventListener?.("message", domListener);
|
|
799
|
+
return () => {
|
|
800
|
+
socket.removeEventListener?.("message", domListener);
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function addWebSocketCloseListener(socket, listener) {
|
|
804
|
+
if (isNodeWebSocket(socket) && socket.on && socket.off) {
|
|
805
|
+
const nodeListener = () => {
|
|
806
|
+
listener();
|
|
807
|
+
};
|
|
808
|
+
socket.on("close", nodeListener);
|
|
809
|
+
return () => {
|
|
810
|
+
socket.off?.("close", nodeListener);
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
const domListener = () => {
|
|
814
|
+
listener();
|
|
815
|
+
};
|
|
816
|
+
socket.addEventListener?.("close", domListener);
|
|
817
|
+
return () => {
|
|
818
|
+
socket.removeEventListener?.("close", domListener);
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function addWebSocketErrorListener(socket, listener) {
|
|
822
|
+
if (isNodeWebSocket(socket) && socket.on && socket.off) {
|
|
823
|
+
const nodeListener = (error) => {
|
|
824
|
+
listener(error);
|
|
825
|
+
};
|
|
826
|
+
socket.on("error", nodeListener);
|
|
827
|
+
return () => {
|
|
828
|
+
socket.off?.("error", nodeListener);
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
const domListener = (event) => {
|
|
832
|
+
listener(event);
|
|
833
|
+
};
|
|
834
|
+
socket.addEventListener?.("error", domListener);
|
|
835
|
+
return () => {
|
|
836
|
+
socket.removeEventListener?.("error", domListener);
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
function closeWebSocketSafely(socket) {
|
|
840
|
+
try {
|
|
841
|
+
socket.close();
|
|
842
|
+
} catch {}
|
|
843
|
+
}
|
|
844
|
+
async function webSocketDataToBytes(data) {
|
|
845
|
+
if (data instanceof Uint8Array) {
|
|
846
|
+
return data;
|
|
847
|
+
}
|
|
848
|
+
if (data instanceof ArrayBuffer) {
|
|
849
|
+
return new Uint8Array(data);
|
|
850
|
+
}
|
|
851
|
+
if (ArrayBuffer.isView(data)) {
|
|
852
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
853
|
+
}
|
|
854
|
+
if (data instanceof Blob) {
|
|
855
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
856
|
+
}
|
|
857
|
+
if (typeof data === "string") {
|
|
858
|
+
return new TextEncoder().encode(data);
|
|
859
|
+
}
|
|
860
|
+
return toBytes(data);
|
|
861
|
+
}
|
|
862
|
+
function streamWebSocketToWritable(socket, output) {
|
|
863
|
+
const writer = output?.getWriter();
|
|
864
|
+
let writeChain = Promise.resolve();
|
|
865
|
+
let finished = false;
|
|
866
|
+
const detachments = [];
|
|
867
|
+
const finish = () => {
|
|
868
|
+
if (finished) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
finished = true;
|
|
872
|
+
for (const detach of detachments) {
|
|
873
|
+
detach();
|
|
874
|
+
}
|
|
875
|
+
if (writer) {
|
|
876
|
+
writeChain.finally(async () => {
|
|
877
|
+
try {
|
|
878
|
+
await writer.close();
|
|
879
|
+
} catch {}
|
|
880
|
+
writer.releaseLock();
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
detachments.push(addWebSocketMessageListener(socket, (data) => {
|
|
885
|
+
if (!writer) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
writeChain = writeChain.then(async () => {
|
|
889
|
+
await writer.write(await webSocketDataToBytes(data));
|
|
890
|
+
}).catch(() => {});
|
|
891
|
+
}));
|
|
892
|
+
detachments.push(addWebSocketCloseListener(socket, finish));
|
|
893
|
+
detachments.push(addWebSocketErrorListener(socket, finish));
|
|
894
|
+
}
|
|
895
|
+
async function sendToWebSocket(socket, data) {
|
|
896
|
+
if (isNodeWebSocket(socket)) {
|
|
897
|
+
await new Promise((resolve, reject) => {
|
|
898
|
+
socket.send(data, (error) => {
|
|
899
|
+
if (error) {
|
|
900
|
+
reject(error);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
resolve();
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
socket.send(data);
|
|
909
|
+
}
|
|
910
|
+
async function* toInputChunks(input) {
|
|
911
|
+
if (input instanceof Uint8Array) {
|
|
912
|
+
yield input;
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (input instanceof ArrayBuffer) {
|
|
916
|
+
yield new Uint8Array(input);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (ArrayBuffer.isView(input)) {
|
|
920
|
+
yield new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (input instanceof Blob) {
|
|
924
|
+
yield new Uint8Array(await input.arrayBuffer());
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const reader = input.getReader();
|
|
928
|
+
try {
|
|
929
|
+
while (true) {
|
|
930
|
+
const { done, value } = await reader.read();
|
|
931
|
+
if (done) {
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
if (value) {
|
|
935
|
+
yield value;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} finally {
|
|
939
|
+
reader.releaseLock();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
async function writeInputToWebSocket(socket, input) {
|
|
943
|
+
if (!input) {
|
|
944
|
+
closeWebSocketSafely(socket);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
for await (const chunk of toInputChunks(input)) {
|
|
949
|
+
await sendToWebSocket(socket, chunk);
|
|
950
|
+
}
|
|
951
|
+
} finally {
|
|
952
|
+
closeWebSocketSafely(socket);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function parseFileHeaders(headers) {
|
|
956
|
+
if (!headers) {
|
|
957
|
+
return {};
|
|
958
|
+
}
|
|
959
|
+
const uidRaw = headers.get("x-incus-uid");
|
|
960
|
+
const gidRaw = headers.get("x-incus-gid");
|
|
961
|
+
const modeRaw = headers.get("x-incus-mode");
|
|
962
|
+
const typeRaw = headers.get("x-incus-type");
|
|
963
|
+
return {
|
|
964
|
+
uid: uidRaw ? Number.parseInt(uidRaw, 10) : undefined,
|
|
965
|
+
gid: gidRaw ? Number.parseInt(gidRaw, 10) : undefined,
|
|
966
|
+
mode: modeRaw ? Number.parseInt(modeRaw, 8) : undefined,
|
|
967
|
+
type: typeRaw ?? undefined
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function createNotImplementedProxy(path) {
|
|
971
|
+
const fn = (..._args) => Promise.reject(new Error(`[Incus.ts] ${path.join(".")}() is not implemented yet`));
|
|
972
|
+
return new Proxy(fn, {
|
|
973
|
+
get(_target, property) {
|
|
974
|
+
if (property === "then") {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (typeof property === "symbol") {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
return createNotImplementedProxy([...path, String(property)]);
|
|
981
|
+
},
|
|
982
|
+
apply() {
|
|
983
|
+
return Promise.reject(new Error(`[Incus.ts] ${path.join(".")}() is not implemented yet`));
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
function createApiWithFallback(root, implemented) {
|
|
988
|
+
return new Proxy(implemented, {
|
|
989
|
+
get(target, property, receiver) {
|
|
990
|
+
if (typeof property === "symbol") {
|
|
991
|
+
return Reflect.get(target, property, receiver);
|
|
992
|
+
}
|
|
993
|
+
if (Reflect.has(target, property)) {
|
|
994
|
+
return Reflect.get(target, property, receiver);
|
|
995
|
+
}
|
|
996
|
+
return createNotImplementedProxy([root, String(property)]);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
function createRemoteOperationFromTarget(targetOperation) {
|
|
1001
|
+
return {
|
|
1002
|
+
wait: async () => {
|
|
1003
|
+
if (targetOperation) {
|
|
1004
|
+
await targetOperation.wait();
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
cancelTarget: async () => {
|
|
1008
|
+
if (targetOperation) {
|
|
1009
|
+
await targetOperation.cancel();
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
target: async () => {
|
|
1013
|
+
if (!targetOperation) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
return targetOperation.refresh();
|
|
1017
|
+
},
|
|
1018
|
+
onUpdate: async (handler) => {
|
|
1019
|
+
if (!targetOperation) {
|
|
1020
|
+
return async () => {};
|
|
1021
|
+
}
|
|
1022
|
+
return targetOperation.onUpdate(handler);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function prepareBody(body) {
|
|
1027
|
+
if (body === undefined || body === null) {
|
|
1028
|
+
return { body: undefined };
|
|
1029
|
+
}
|
|
1030
|
+
if (body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof ReadableStream || typeof body === "string" || body instanceof URLSearchParams || body instanceof FormData) {
|
|
1031
|
+
return { body };
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
body: JSON.stringify(body),
|
|
1035
|
+
contentType: "application/json"
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
async function prepareUnixBody(body) {
|
|
1039
|
+
if (body === undefined || body === null) {
|
|
1040
|
+
return {};
|
|
1041
|
+
}
|
|
1042
|
+
if (typeof body === "string") {
|
|
1043
|
+
return { bodyBytes: new TextEncoder().encode(body), contentType: "text/plain" };
|
|
1044
|
+
}
|
|
1045
|
+
if (body instanceof Uint8Array) {
|
|
1046
|
+
return { bodyBytes: body };
|
|
1047
|
+
}
|
|
1048
|
+
if (body instanceof ArrayBuffer) {
|
|
1049
|
+
return { bodyBytes: new Uint8Array(body) };
|
|
1050
|
+
}
|
|
1051
|
+
if (ArrayBuffer.isView(body)) {
|
|
1052
|
+
return { bodyBytes: new Uint8Array(body.buffer, body.byteOffset, body.byteLength) };
|
|
1053
|
+
}
|
|
1054
|
+
if (body instanceof Blob) {
|
|
1055
|
+
return { bodyBytes: new Uint8Array(await body.arrayBuffer()) };
|
|
1056
|
+
}
|
|
1057
|
+
if (body instanceof ReadableStream) {
|
|
1058
|
+
throw new Error("[Incus.ts] ReadableStream body over Unix socket is not implemented yet");
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
bodyBytes: new TextEncoder().encode(JSON.stringify(body)),
|
|
1062
|
+
contentType: "application/json"
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
class IncusImageClient {
|
|
1067
|
+
transport;
|
|
1068
|
+
endpoint;
|
|
1069
|
+
options;
|
|
1070
|
+
connection;
|
|
1071
|
+
images;
|
|
1072
|
+
raw;
|
|
1073
|
+
contextState;
|
|
1074
|
+
kind;
|
|
1075
|
+
serverSnapshot;
|
|
1076
|
+
constructor(transport, endpoint, options = {}, context = {}, kind = "incus") {
|
|
1077
|
+
this.transport = transport;
|
|
1078
|
+
this.endpoint = endpoint;
|
|
1079
|
+
this.options = options;
|
|
1080
|
+
this.contextState = context;
|
|
1081
|
+
this.kind = kind;
|
|
1082
|
+
this.connection = this.createConnectionApi();
|
|
1083
|
+
this.raw = this.createRawApi();
|
|
1084
|
+
this.images = this.createImagesApi();
|
|
1085
|
+
}
|
|
1086
|
+
get context() {
|
|
1087
|
+
return this.contextState;
|
|
1088
|
+
}
|
|
1089
|
+
disconnect() {
|
|
1090
|
+
this.transport.close?.();
|
|
1091
|
+
}
|
|
1092
|
+
async requestTransport(method, path, options = {}) {
|
|
1093
|
+
const { applyContext = true, ...rest } = options;
|
|
1094
|
+
return this.transport.request(method, path, {
|
|
1095
|
+
...rest,
|
|
1096
|
+
context: applyContext ? this.contextState : undefined
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
async requestEnvelope(method, path, options = {}) {
|
|
1100
|
+
const response = await this.requestTransport(method, path, options);
|
|
1101
|
+
const payload = response.data;
|
|
1102
|
+
if (isIncusEnvelope(payload)) {
|
|
1103
|
+
if (payload.type === "error" || response.status >= 400) {
|
|
1104
|
+
throw new IncusApiError(payload.error ?? `Incus request failed (${response.status})`, response.status, {
|
|
1105
|
+
statusCode: payload.status_code,
|
|
1106
|
+
errorCode: payload.error_code,
|
|
1107
|
+
details: payload.metadata
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
value: payload.metadata,
|
|
1112
|
+
etag: response.etag,
|
|
1113
|
+
operation: payload.operation,
|
|
1114
|
+
status: response.status,
|
|
1115
|
+
headers: response.headers
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
if (response.status >= 400) {
|
|
1119
|
+
throw new IncusApiError(typeof payload === "string" ? payload : `Incus request failed (${response.status})`, response.status, { details: payload });
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
value: payload,
|
|
1123
|
+
etag: response.etag,
|
|
1124
|
+
status: response.status,
|
|
1125
|
+
headers: response.headers
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
async requestBinary(method, path, options = {}) {
|
|
1129
|
+
const response = await this.requestTransport(method, path, options);
|
|
1130
|
+
const payload = response.data;
|
|
1131
|
+
if (isIncusEnvelope(payload)) {
|
|
1132
|
+
if (payload.type === "error" || response.status >= 400) {
|
|
1133
|
+
throw new IncusApiError(payload.error ?? `Incus request failed (${response.status})`, response.status, {
|
|
1134
|
+
statusCode: payload.status_code,
|
|
1135
|
+
errorCode: payload.error_code,
|
|
1136
|
+
details: payload.metadata
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
value: toBytes(payload.metadata),
|
|
1141
|
+
etag: response.etag,
|
|
1142
|
+
operation: payload.operation,
|
|
1143
|
+
status: response.status,
|
|
1144
|
+
headers: response.headers
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
if (response.status >= 400) {
|
|
1148
|
+
throw new IncusApiError(typeof payload === "string" ? payload : `Incus request failed (${response.status})`, response.status, { details: payload });
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
value: toBytes(payload),
|
|
1152
|
+
etag: response.etag,
|
|
1153
|
+
status: response.status,
|
|
1154
|
+
headers: response.headers
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
async createOperationFromRequest(method, path, options = {}) {
|
|
1158
|
+
const result = await this.requestEnvelope(method, path, options);
|
|
1159
|
+
return this.createOperationFromEnvelopeResult(method, path, result);
|
|
1160
|
+
}
|
|
1161
|
+
createOperationFromEnvelopeResult(method, path, result) {
|
|
1162
|
+
const operationId = parseOperationIdFromEnvelope(result);
|
|
1163
|
+
if (!operationId) {
|
|
1164
|
+
throw new Error(`[Incus.ts] Missing operation id in response for ${method.toUpperCase()} ${path}`);
|
|
1165
|
+
}
|
|
1166
|
+
return this.createOperation(operationId);
|
|
1167
|
+
}
|
|
1168
|
+
createOperation(id) {
|
|
1169
|
+
return {
|
|
1170
|
+
id,
|
|
1171
|
+
wait: async (options = {}) => {
|
|
1172
|
+
const result = await this.requestEnvelope("GET", `${operationPath(id)}/wait`, {
|
|
1173
|
+
query: {
|
|
1174
|
+
timeout: options.timeoutSeconds ?? -1
|
|
1175
|
+
},
|
|
1176
|
+
signal: options.signal
|
|
1177
|
+
});
|
|
1178
|
+
const operation = toRecord(result.value);
|
|
1179
|
+
assertOperationSuccess(operation, id);
|
|
1180
|
+
return operation;
|
|
1181
|
+
},
|
|
1182
|
+
cancel: async () => {
|
|
1183
|
+
await this.requestEnvelope("DELETE", operationPath(id));
|
|
1184
|
+
},
|
|
1185
|
+
refresh: async () => {
|
|
1186
|
+
const result = await this.requestEnvelope("GET", operationPath(id));
|
|
1187
|
+
return toRecord(result.value);
|
|
1188
|
+
},
|
|
1189
|
+
websocket: async (secret) => {
|
|
1190
|
+
return this.openWebsocket(`${operationPath(id)}/websocket?secret=${encodeURIComponent(secret)}`);
|
|
1191
|
+
},
|
|
1192
|
+
onUpdate: async (handler) => {
|
|
1193
|
+
const timer = setInterval(async () => {
|
|
1194
|
+
try {
|
|
1195
|
+
const operation = await this.requestEnvelope("GET", operationPath(id));
|
|
1196
|
+
handler(toRecord(operation.value));
|
|
1197
|
+
} catch {}
|
|
1198
|
+
}, 1000);
|
|
1199
|
+
return async () => {
|
|
1200
|
+
clearInterval(timer);
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
async openWebsocket(path, options = {}) {
|
|
1206
|
+
if (!this.transport.websocket) {
|
|
1207
|
+
throw new Error("[Incus.ts] Transport does not support websocket connections");
|
|
1208
|
+
}
|
|
1209
|
+
let finalPath = path;
|
|
1210
|
+
if (options.applyContext !== false) {
|
|
1211
|
+
const url = new URL(normalizePath(path), "http://incus.local");
|
|
1212
|
+
if (this.context.target && !url.searchParams.has("target")) {
|
|
1213
|
+
url.searchParams.set("target", this.context.target);
|
|
1214
|
+
}
|
|
1215
|
+
if (this.context.project && !url.searchParams.has("project") && !url.searchParams.has("all-projects")) {
|
|
1216
|
+
url.searchParams.set("project", this.context.project);
|
|
1217
|
+
}
|
|
1218
|
+
finalPath = `${url.pathname}${url.search}`;
|
|
1219
|
+
}
|
|
1220
|
+
return this.transport.websocket(finalPath);
|
|
1221
|
+
}
|
|
1222
|
+
async getServerSnapshot() {
|
|
1223
|
+
if (this.serverSnapshot) {
|
|
1224
|
+
return this.serverSnapshot;
|
|
1225
|
+
}
|
|
1226
|
+
if (this.kind === "simple-streams" || this.kind === "incus-public") {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
const server = await this.requestEnvelope("GET", "/1.0");
|
|
1231
|
+
this.serverSnapshot = toRecord(server.value);
|
|
1232
|
+
return this.serverSnapshot;
|
|
1233
|
+
} catch {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
createConnectionApi() {
|
|
1238
|
+
return createApiWithFallback("connection", {
|
|
1239
|
+
info: async () => {
|
|
1240
|
+
const server = await this.getServerSnapshot();
|
|
1241
|
+
const env = toRecord(server?.environment);
|
|
1242
|
+
const addresses = Array.isArray(env.addresses) ? env.addresses.filter((entry) => typeof entry === "string") : [];
|
|
1243
|
+
const httpsAddresses = addresses.filter((entry) => !entry.startsWith(":")).map((entry) => `https://${entry}`);
|
|
1244
|
+
const socketPath = this.endpoint.startsWith("unix://") ? this.endpoint.replace(/^unix:\/\//, "") : undefined;
|
|
1245
|
+
return {
|
|
1246
|
+
addresses: httpsAddresses,
|
|
1247
|
+
certificate: typeof env.certificate === "string" ? env.certificate : undefined,
|
|
1248
|
+
protocol: this.kind === "simple-streams" ? "simplestreams" : "incus",
|
|
1249
|
+
url: this.endpoint.startsWith("unix://") ? undefined : this.endpoint,
|
|
1250
|
+
socketPath,
|
|
1251
|
+
project: this.context.project ?? "default",
|
|
1252
|
+
target: this.context.target ?? (typeof env.server_name === "string" ? env.server_name : undefined)
|
|
1253
|
+
};
|
|
1254
|
+
},
|
|
1255
|
+
httpClient: async () => this.options.fetch ?? fetch,
|
|
1256
|
+
doHttp: async (request) => {
|
|
1257
|
+
const fetchImpl = this.options.fetch ?? fetch;
|
|
1258
|
+
return fetchImpl(request);
|
|
1259
|
+
},
|
|
1260
|
+
disconnect: () => this.disconnect()
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
createRawApi() {
|
|
1264
|
+
return createApiWithFallback("raw", {
|
|
1265
|
+
query: async (method, path, body, options = {}) => {
|
|
1266
|
+
const result = await this.requestEnvelope(method, path, {
|
|
1267
|
+
body,
|
|
1268
|
+
etag: options.etag,
|
|
1269
|
+
applyContext: false
|
|
1270
|
+
});
|
|
1271
|
+
return { value: result.value, etag: result.etag };
|
|
1272
|
+
},
|
|
1273
|
+
websocket: async (path) => this.openWebsocket(path, { applyContext: false }),
|
|
1274
|
+
operation: async (method, path, body, options = {}) => {
|
|
1275
|
+
return this.createOperationFromRequest(method, path, {
|
|
1276
|
+
body,
|
|
1277
|
+
etag: options.etag,
|
|
1278
|
+
applyContext: false
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
createImageAliasesApi() {
|
|
1284
|
+
return createApiWithFallback("images.aliases", {
|
|
1285
|
+
list: async () => {
|
|
1286
|
+
const result = await this.requestEnvelope("GET", "/1.0/images/aliases", { query: { recursion: 1 } });
|
|
1287
|
+
return toRecordArray(result.value);
|
|
1288
|
+
},
|
|
1289
|
+
names: async () => {
|
|
1290
|
+
const result = await this.requestEnvelope("GET", "/1.0/images/aliases");
|
|
1291
|
+
return urlsToResourceNames("/images/aliases", toStringArray(result.value));
|
|
1292
|
+
},
|
|
1293
|
+
get: async (name) => {
|
|
1294
|
+
const result = await this.requestEnvelope("GET", `/1.0/images/aliases/${encodeURIComponent(name)}`);
|
|
1295
|
+
return { value: toRecord(result.value), etag: result.etag };
|
|
1296
|
+
},
|
|
1297
|
+
getArchitectures: async (name) => {
|
|
1298
|
+
const alias = await this.requestEnvelope("GET", `/1.0/images/aliases/${encodeURIComponent(name)}`);
|
|
1299
|
+
const target = toRecord(alias.value).target;
|
|
1300
|
+
if (typeof target !== "string") {
|
|
1301
|
+
return {};
|
|
1302
|
+
}
|
|
1303
|
+
const image = await this.requestEnvelope("GET", `/1.0/images/${encodeURIComponent(target)}`);
|
|
1304
|
+
const architecture = toRecord(image.value).architecture;
|
|
1305
|
+
if (typeof architecture !== "string") {
|
|
1306
|
+
return {};
|
|
1307
|
+
}
|
|
1308
|
+
return { [architecture]: toRecord(alias.value) };
|
|
1309
|
+
},
|
|
1310
|
+
create: async (alias) => {
|
|
1311
|
+
await this.requestEnvelope("POST", "/1.0/images/aliases", { body: alias });
|
|
1312
|
+
},
|
|
1313
|
+
update: async (name, alias, options = {}) => {
|
|
1314
|
+
await this.requestEnvelope("PUT", `/1.0/images/aliases/${encodeURIComponent(name)}`, { body: alias, etag: options.etag });
|
|
1315
|
+
},
|
|
1316
|
+
rename: async (name, alias) => {
|
|
1317
|
+
await this.requestEnvelope("POST", `/1.0/images/aliases/${encodeURIComponent(name)}`, { body: alias });
|
|
1318
|
+
},
|
|
1319
|
+
remove: async (name) => {
|
|
1320
|
+
await this.requestEnvelope("DELETE", `/1.0/images/aliases/${encodeURIComponent(name)}`);
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
createImagesApi() {
|
|
1325
|
+
if (this.kind === "simple-streams") {
|
|
1326
|
+
return createApiWithFallback("images", {
|
|
1327
|
+
aliases: createNotImplementedProxy(["images", "aliases"])
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
return createApiWithFallback("images", {
|
|
1331
|
+
list: async (options = {}) => {
|
|
1332
|
+
const query = {
|
|
1333
|
+
recursion: 1
|
|
1334
|
+
};
|
|
1335
|
+
if (options.allProjects) {
|
|
1336
|
+
query["all-projects"] = true;
|
|
1337
|
+
}
|
|
1338
|
+
if (options.filter && options.filter.length > 0) {
|
|
1339
|
+
query.filter = parseFilters(options.filter);
|
|
1340
|
+
}
|
|
1341
|
+
const result = await this.requestEnvelope("GET", "/1.0/images", {
|
|
1342
|
+
query
|
|
1343
|
+
});
|
|
1344
|
+
return toRecordArray(result.value);
|
|
1345
|
+
},
|
|
1346
|
+
fingerprints: async (options = {}) => {
|
|
1347
|
+
if (options.filter?.length || options.allProjects) {
|
|
1348
|
+
const images = await this.images.list(options);
|
|
1349
|
+
return images.map((image) => image.fingerprint).filter((entry) => typeof entry === "string");
|
|
1350
|
+
}
|
|
1351
|
+
const result = await this.requestEnvelope("GET", "/1.0/images");
|
|
1352
|
+
return urlsToResourceNames("/images", toStringArray(result.value));
|
|
1353
|
+
},
|
|
1354
|
+
get: async (fingerprint, options = {}) => {
|
|
1355
|
+
const result = await this.requestEnvelope("GET", `/1.0/images/${encodeURIComponent(fingerprint)}`, {
|
|
1356
|
+
query: options.secret ? { secret: options.secret } : undefined
|
|
1357
|
+
});
|
|
1358
|
+
return { value: toRecord(result.value), etag: result.etag };
|
|
1359
|
+
},
|
|
1360
|
+
create: async (image, upload) => {
|
|
1361
|
+
const body = upload ? { ...image, upload } : image;
|
|
1362
|
+
return this.createOperationFromRequest("POST", "/1.0/images", { body });
|
|
1363
|
+
},
|
|
1364
|
+
copyFrom: async (_source, _image, _options) => createRemoteOperationFromTarget(null),
|
|
1365
|
+
update: async (fingerprint, image, options = {}) => {
|
|
1366
|
+
await this.requestEnvelope("PUT", `/1.0/images/${encodeURIComponent(fingerprint)}`, {
|
|
1367
|
+
body: image,
|
|
1368
|
+
etag: options.etag
|
|
1369
|
+
});
|
|
1370
|
+
},
|
|
1371
|
+
remove: async (fingerprint) => {
|
|
1372
|
+
return this.createOperationFromRequest("DELETE", `/1.0/images/${encodeURIComponent(fingerprint)}`);
|
|
1373
|
+
},
|
|
1374
|
+
refresh: async (fingerprint) => {
|
|
1375
|
+
return this.createOperationFromRequest("POST", `/1.0/images/${encodeURIComponent(fingerprint)}/refresh`);
|
|
1376
|
+
},
|
|
1377
|
+
createSecret: async (fingerprint) => {
|
|
1378
|
+
return this.createOperationFromRequest("POST", `/1.0/images/${encodeURIComponent(fingerprint)}/secret`);
|
|
1379
|
+
},
|
|
1380
|
+
export: async (fingerprint, request) => {
|
|
1381
|
+
return this.createOperationFromRequest("POST", `/1.0/images/${encodeURIComponent(fingerprint)}/export`, { body: request ?? {} });
|
|
1382
|
+
},
|
|
1383
|
+
aliases: this.createImageAliasesApi()
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
class IncusClient extends IncusImageClient {
|
|
1389
|
+
server;
|
|
1390
|
+
certificates;
|
|
1391
|
+
instances;
|
|
1392
|
+
events;
|
|
1393
|
+
metadata;
|
|
1394
|
+
networks;
|
|
1395
|
+
operations;
|
|
1396
|
+
profiles;
|
|
1397
|
+
projects;
|
|
1398
|
+
storage;
|
|
1399
|
+
cluster;
|
|
1400
|
+
warnings;
|
|
1401
|
+
oidc;
|
|
1402
|
+
constructor(transport, endpoint, options = {}, context = {}, kind = "incus") {
|
|
1403
|
+
super(transport, endpoint, options, context, kind);
|
|
1404
|
+
this.server = this.createServerApi();
|
|
1405
|
+
this.certificates = createNotImplementedProxy(["certificates"]);
|
|
1406
|
+
this.instances = this.createInstancesApi();
|
|
1407
|
+
this.events = createNotImplementedProxy(["events"]);
|
|
1408
|
+
this.metadata = createApiWithFallback("metadata", {
|
|
1409
|
+
configuration: async () => {
|
|
1410
|
+
const result = await this.requestEnvelope("GET", "/1.0/metadata/configuration");
|
|
1411
|
+
return toRecord(result.value);
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
this.networks = createNotImplementedProxy(["networks"]);
|
|
1415
|
+
this.operations = this.createOperationsApi();
|
|
1416
|
+
this.profiles = createNotImplementedProxy(["profiles"]);
|
|
1417
|
+
this.projects = createNotImplementedProxy(["projects"]);
|
|
1418
|
+
this.storage = createNotImplementedProxy(["storage"]);
|
|
1419
|
+
this.cluster = createNotImplementedProxy(["cluster"]);
|
|
1420
|
+
this.warnings = createNotImplementedProxy(["warnings"]);
|
|
1421
|
+
this.oidc = createApiWithFallback("oidc", {
|
|
1422
|
+
tokens: async () => {
|
|
1423
|
+
if (!this.options.oidcTokens) {
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
return { ...this.options.oidcTokens };
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
withContext(contextPatch) {
|
|
1431
|
+
return new IncusClient(this.transport, this.endpoint, this.options, {
|
|
1432
|
+
...this.context,
|
|
1433
|
+
...contextPatch
|
|
1434
|
+
}, this.kind);
|
|
1435
|
+
}
|
|
1436
|
+
project(name) {
|
|
1437
|
+
return this.withContext({ project: name });
|
|
1438
|
+
}
|
|
1439
|
+
target(name) {
|
|
1440
|
+
return this.withContext({ target: name });
|
|
1441
|
+
}
|
|
1442
|
+
requireAuthenticated(authenticated = true) {
|
|
1443
|
+
return this.withContext({ requireAuthenticated: authenticated });
|
|
1444
|
+
}
|
|
1445
|
+
createServerApi() {
|
|
1446
|
+
return createApiWithFallback("server", {
|
|
1447
|
+
metrics: async () => {
|
|
1448
|
+
const result = await this.requestEnvelope("GET", "/1.0/metrics");
|
|
1449
|
+
return typeof result.value === "string" ? result.value : new TextDecoder().decode(toBytes(result.value));
|
|
1450
|
+
},
|
|
1451
|
+
get: async () => {
|
|
1452
|
+
const result = await this.requestEnvelope("GET", "/1.0");
|
|
1453
|
+
return { value: toRecord(result.value), etag: result.etag };
|
|
1454
|
+
},
|
|
1455
|
+
resources: async () => {
|
|
1456
|
+
const result = await this.requestEnvelope("GET", "/1.0/resources");
|
|
1457
|
+
return toRecord(result.value);
|
|
1458
|
+
},
|
|
1459
|
+
update: async (server, options = {}) => {
|
|
1460
|
+
await this.requestEnvelope("PUT", "/1.0", {
|
|
1461
|
+
body: server,
|
|
1462
|
+
etag: options.etag
|
|
1463
|
+
});
|
|
1464
|
+
},
|
|
1465
|
+
applyPreseed: async (config) => {
|
|
1466
|
+
await this.requestEnvelope("PUT", "/1.0", { body: config });
|
|
1467
|
+
},
|
|
1468
|
+
hasExtension: async (extension) => {
|
|
1469
|
+
const result = await this.requestEnvelope("GET", "/1.0");
|
|
1470
|
+
const server = toRecord(result.value);
|
|
1471
|
+
const apiExtensions = server.api_extensions;
|
|
1472
|
+
if (!Array.isArray(apiExtensions)) {
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
return apiExtensions.some((entry) => entry === extension);
|
|
1476
|
+
},
|
|
1477
|
+
isClustered: async () => {
|
|
1478
|
+
const result = await this.requestEnvelope("GET", "/1.0");
|
|
1479
|
+
const env = toRecord(toRecord(result.value).environment);
|
|
1480
|
+
return env.server_clustered === true;
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
createOperationsApi() {
|
|
1485
|
+
return createApiWithFallback("operations", {
|
|
1486
|
+
uuids: async () => {
|
|
1487
|
+
const result = await this.requestEnvelope("GET", "/1.0/operations");
|
|
1488
|
+
return urlsToResourceNames("/operations", toStringArray(result.value));
|
|
1489
|
+
},
|
|
1490
|
+
list: async (options = {}) => {
|
|
1491
|
+
const result = await this.requestEnvelope("GET", "/1.0/operations", {
|
|
1492
|
+
query: {
|
|
1493
|
+
recursion: 1,
|
|
1494
|
+
"all-projects": options.allProjects ? "true" : undefined
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
return flattenOperationMap(result.value);
|
|
1498
|
+
},
|
|
1499
|
+
get: async (uuid) => {
|
|
1500
|
+
const result = await this.requestEnvelope("GET", `/1.0/operations/${encodeURIComponent(uuid)}`);
|
|
1501
|
+
return { value: toRecord(result.value), etag: result.etag };
|
|
1502
|
+
},
|
|
1503
|
+
wait: async (uuid, timeoutSeconds = -1) => {
|
|
1504
|
+
const result = await this.requestEnvelope("GET", `/1.0/operations/${encodeURIComponent(uuid)}/wait`, {
|
|
1505
|
+
query: { timeout: timeoutSeconds }
|
|
1506
|
+
});
|
|
1507
|
+
const operation = toRecord(result.value);
|
|
1508
|
+
assertOperationSuccess(operation, uuid);
|
|
1509
|
+
return { value: operation, etag: result.etag };
|
|
1510
|
+
},
|
|
1511
|
+
waitWithSecret: async (uuid, secret, timeoutSeconds = -1) => {
|
|
1512
|
+
const result = await this.requestEnvelope("GET", `/1.0/operations/${encodeURIComponent(uuid)}/wait`, {
|
|
1513
|
+
query: {
|
|
1514
|
+
secret,
|
|
1515
|
+
timeout: timeoutSeconds
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
const operation = toRecord(result.value);
|
|
1519
|
+
assertOperationSuccess(operation, uuid);
|
|
1520
|
+
return { value: operation, etag: result.etag };
|
|
1521
|
+
},
|
|
1522
|
+
websocket: async (uuid, secret) => {
|
|
1523
|
+
const query = secret ? `?secret=${encodeURIComponent(secret)}` : "";
|
|
1524
|
+
return this.openWebsocket(`/1.0/operations/${encodeURIComponent(uuid)}/websocket${query}`);
|
|
1525
|
+
},
|
|
1526
|
+
remove: async (uuid) => {
|
|
1527
|
+
await this.requestEnvelope("DELETE", `/1.0/operations/${encodeURIComponent(uuid)}`);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
shouldAttachExecIO(options) {
|
|
1532
|
+
return Boolean(options?.stdin || options?.stdout || options?.stderr || options?.onControl);
|
|
1533
|
+
}
|
|
1534
|
+
async getExecFds(operation, initialOperationValue) {
|
|
1535
|
+
const initial = extractExecFds(initialOperationValue);
|
|
1536
|
+
if (Object.keys(initial).length > 0) {
|
|
1537
|
+
return initial;
|
|
1538
|
+
}
|
|
1539
|
+
const refreshed = await operation.refresh();
|
|
1540
|
+
return extractExecFds(refreshed);
|
|
1541
|
+
}
|
|
1542
|
+
async attachExecWebsockets(operation, request, options, initialOperationValue) {
|
|
1543
|
+
const fds = await this.getExecFds(operation, initialOperationValue ?? {});
|
|
1544
|
+
if (Object.keys(fds).length === 0) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const openChannel = async (name) => {
|
|
1548
|
+
const secret = fds[name];
|
|
1549
|
+
if (!secret) {
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
const socket = await operation.websocket(secret);
|
|
1553
|
+
if (options?.signal) {
|
|
1554
|
+
options.signal.addEventListener("abort", () => {
|
|
1555
|
+
closeWebSocketSafely(socket);
|
|
1556
|
+
}, { once: true });
|
|
1557
|
+
}
|
|
1558
|
+
return socket;
|
|
1559
|
+
};
|
|
1560
|
+
const controlSocket = await openChannel("control");
|
|
1561
|
+
if (controlSocket) {
|
|
1562
|
+
streamWebSocketToWritable(controlSocket);
|
|
1563
|
+
options?.onControl?.(controlSocket);
|
|
1564
|
+
}
|
|
1565
|
+
if (isInteractiveExecRequest(request)) {
|
|
1566
|
+
const interactiveSocket = await openChannel("0");
|
|
1567
|
+
if (!interactiveSocket) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
streamWebSocketToWritable(interactiveSocket, options?.stdout);
|
|
1571
|
+
writeInputToWebSocket(interactiveSocket, options?.stdin);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const stdinSocket = await openChannel("0");
|
|
1575
|
+
if (stdinSocket) {
|
|
1576
|
+
writeInputToWebSocket(stdinSocket, options?.stdin);
|
|
1577
|
+
}
|
|
1578
|
+
const stdoutSocket = await openChannel("1");
|
|
1579
|
+
if (stdoutSocket) {
|
|
1580
|
+
streamWebSocketToWritable(stdoutSocket, options?.stdout);
|
|
1581
|
+
}
|
|
1582
|
+
const stderrSocket = await openChannel("2");
|
|
1583
|
+
if (stderrSocket) {
|
|
1584
|
+
streamWebSocketToWritable(stderrSocket, options?.stderr);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
createInstancesApi() {
|
|
1588
|
+
const logs = createApiWithFallback("instances.logs", {
|
|
1589
|
+
list: async (instanceName) => {
|
|
1590
|
+
const result = await this.requestEnvelope("GET", `/1.0/instances/${encodeURIComponent(instanceName)}/logs`);
|
|
1591
|
+
return urlsToResourceNames(`/instances/${encodeURIComponent(instanceName)}/logs`, toStringArray(result.value));
|
|
1592
|
+
},
|
|
1593
|
+
get: async (instanceName, filename) => {
|
|
1594
|
+
const result = await this.requestBinary("GET", `/1.0/instances/${encodeURIComponent(instanceName)}/logs/${encodeURIComponent(filename)}`);
|
|
1595
|
+
return toReadableStream(result.value);
|
|
1596
|
+
},
|
|
1597
|
+
remove: async (instanceName, filename) => {
|
|
1598
|
+
await this.requestEnvelope("DELETE", `/1.0/instances/${encodeURIComponent(instanceName)}/logs/${encodeURIComponent(filename)}`);
|
|
1599
|
+
},
|
|
1600
|
+
getConsole: async (instanceName) => {
|
|
1601
|
+
const result = await this.requestBinary("GET", `/1.0/instances/${encodeURIComponent(instanceName)}/console`);
|
|
1602
|
+
return toReadableStream(result.value);
|
|
1603
|
+
},
|
|
1604
|
+
removeConsole: async (instanceName) => {
|
|
1605
|
+
await this.requestEnvelope("DELETE", `/1.0/instances/${encodeURIComponent(instanceName)}/console`);
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
const files = createApiWithFallback("instances.files", {
|
|
1609
|
+
get: async (instanceName, path) => {
|
|
1610
|
+
const response = await this.requestTransport("GET", `/1.0/instances/${encodeURIComponent(instanceName)}/files`, {
|
|
1611
|
+
query: { path }
|
|
1612
|
+
});
|
|
1613
|
+
const headers = parseFileHeaders(response.headers);
|
|
1614
|
+
if ((headers.type ?? "").toLowerCase() === "directory") {
|
|
1615
|
+
const decoded = isIncusEnvelope(response.data) ? toStringArray(response.data.metadata) : [];
|
|
1616
|
+
return {
|
|
1617
|
+
stream: toReadableStream(new Uint8Array),
|
|
1618
|
+
uid: headers.uid,
|
|
1619
|
+
gid: headers.gid,
|
|
1620
|
+
mode: headers.mode,
|
|
1621
|
+
type: headers.type,
|
|
1622
|
+
entries: decoded
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
if (response.status >= 400) {
|
|
1626
|
+
if (isIncusEnvelope(response.data)) {
|
|
1627
|
+
throw new IncusApiError(response.data.error ?? `Incus request failed (${response.status})`, response.status, {
|
|
1628
|
+
statusCode: response.data.status_code,
|
|
1629
|
+
errorCode: response.data.error_code,
|
|
1630
|
+
details: response.data.metadata
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
throw new IncusApiError(`Incus request failed (${response.status})`, response.status, {
|
|
1634
|
+
details: response.data
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
stream: toReadableStream(toBytes(response.data)),
|
|
1639
|
+
uid: headers.uid,
|
|
1640
|
+
gid: headers.gid,
|
|
1641
|
+
mode: headers.mode,
|
|
1642
|
+
type: headers.type
|
|
1643
|
+
};
|
|
1644
|
+
},
|
|
1645
|
+
put: async (instanceName, path, options) => {
|
|
1646
|
+
const headers = new Headers;
|
|
1647
|
+
if (options.uid !== undefined) {
|
|
1648
|
+
headers.set("X-Incus-uid", String(options.uid));
|
|
1649
|
+
}
|
|
1650
|
+
if (options.gid !== undefined) {
|
|
1651
|
+
headers.set("X-Incus-gid", String(options.gid));
|
|
1652
|
+
}
|
|
1653
|
+
if (options.mode !== undefined) {
|
|
1654
|
+
headers.set("X-Incus-mode", options.mode.toString(8).padStart(4, "0"));
|
|
1655
|
+
}
|
|
1656
|
+
if (options.type) {
|
|
1657
|
+
headers.set("X-Incus-type", options.type);
|
|
1658
|
+
}
|
|
1659
|
+
if (options.writeMode) {
|
|
1660
|
+
headers.set("X-Incus-write", options.writeMode);
|
|
1661
|
+
}
|
|
1662
|
+
await this.requestEnvelope("POST", `/1.0/instances/${encodeURIComponent(instanceName)}/files`, {
|
|
1663
|
+
query: { path },
|
|
1664
|
+
body: options.content,
|
|
1665
|
+
headers
|
|
1666
|
+
});
|
|
1667
|
+
},
|
|
1668
|
+
remove: async (instanceName, path) => {
|
|
1669
|
+
await this.requestEnvelope("DELETE", `/1.0/instances/${encodeURIComponent(instanceName)}/files`, {
|
|
1670
|
+
query: { path }
|
|
1671
|
+
});
|
|
1672
|
+
},
|
|
1673
|
+
sftp: async () => {
|
|
1674
|
+
throw new Error("[Incus.ts] Instance SFTP helper is not implemented yet");
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
return createApiWithFallback("instances", {
|
|
1678
|
+
names: async (options = {}) => {
|
|
1679
|
+
if (options.allProjects) {
|
|
1680
|
+
const query2 = {
|
|
1681
|
+
recursion: 1,
|
|
1682
|
+
"all-projects": "true"
|
|
1683
|
+
};
|
|
1684
|
+
if (options.type) {
|
|
1685
|
+
query2["instance-type"] = options.type;
|
|
1686
|
+
}
|
|
1687
|
+
const result2 = await this.requestEnvelope("GET", "/1.0/instances", { query: query2 });
|
|
1688
|
+
const records = toRecordArray(result2.value);
|
|
1689
|
+
const grouped = {};
|
|
1690
|
+
for (const record of records) {
|
|
1691
|
+
const project = typeof record.project === "string" ? record.project : "default";
|
|
1692
|
+
const name = typeof record.name === "string" ? record.name : undefined;
|
|
1693
|
+
if (!name) {
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
grouped[project] ??= [];
|
|
1697
|
+
grouped[project].push(name);
|
|
1698
|
+
}
|
|
1699
|
+
return grouped;
|
|
1700
|
+
}
|
|
1701
|
+
const query = {};
|
|
1702
|
+
if (options.type) {
|
|
1703
|
+
query["instance-type"] = options.type;
|
|
1704
|
+
}
|
|
1705
|
+
const result = await this.requestEnvelope("GET", "/1.0/instances", {
|
|
1706
|
+
query
|
|
1707
|
+
});
|
|
1708
|
+
return urlsToResourceNames("/instances", toStringArray(result.value));
|
|
1709
|
+
},
|
|
1710
|
+
list: async (options = {}) => {
|
|
1711
|
+
const query = {
|
|
1712
|
+
recursion: options.full ? 2 : 1
|
|
1713
|
+
};
|
|
1714
|
+
if (options.type) {
|
|
1715
|
+
query["instance-type"] = options.type;
|
|
1716
|
+
}
|
|
1717
|
+
if (options.allProjects) {
|
|
1718
|
+
query["all-projects"] = "true";
|
|
1719
|
+
}
|
|
1720
|
+
if (options.filter && options.filter.length > 0) {
|
|
1721
|
+
query.filter = parseFilters(options.filter);
|
|
1722
|
+
}
|
|
1723
|
+
const result = await this.requestEnvelope("GET", "/1.0/instances", { query });
|
|
1724
|
+
return toRecordArray(result.value);
|
|
1725
|
+
},
|
|
1726
|
+
get: async (name, options = {}) => {
|
|
1727
|
+
const result = await this.requestEnvelope("GET", `/1.0/instances/${encodeURIComponent(name)}`, {
|
|
1728
|
+
query: options.full ? { recursion: 1 } : undefined
|
|
1729
|
+
});
|
|
1730
|
+
return { value: toRecord(result.value), etag: result.etag };
|
|
1731
|
+
},
|
|
1732
|
+
create: async (instance) => {
|
|
1733
|
+
return this.createOperationFromRequest("POST", "/1.0/instances", { body: instance });
|
|
1734
|
+
},
|
|
1735
|
+
createFromImage: async (_source, _image, _request) => createRemoteOperationFromTarget(null),
|
|
1736
|
+
createFromBackup: async (args) => {
|
|
1737
|
+
return this.createOperationFromRequest("POST", "/1.0/instances", { body: args });
|
|
1738
|
+
},
|
|
1739
|
+
copyFrom: async (_source, _instance, _options) => createRemoteOperationFromTarget(null),
|
|
1740
|
+
update: async (name, instance, options = {}) => {
|
|
1741
|
+
return this.createOperationFromRequest("PUT", `/1.0/instances/${encodeURIComponent(name)}`, {
|
|
1742
|
+
body: instance,
|
|
1743
|
+
etag: options.etag
|
|
1744
|
+
});
|
|
1745
|
+
},
|
|
1746
|
+
rename: async (name, request) => {
|
|
1747
|
+
return this.createOperationFromRequest("POST", `/1.0/instances/${encodeURIComponent(name)}`, { body: request });
|
|
1748
|
+
},
|
|
1749
|
+
migrate: async (name, request) => {
|
|
1750
|
+
return this.createOperationFromRequest("POST", `/1.0/instances/${encodeURIComponent(name)}`, { body: request });
|
|
1751
|
+
},
|
|
1752
|
+
remove: async (name) => {
|
|
1753
|
+
return this.createOperationFromRequest("DELETE", `/1.0/instances/${encodeURIComponent(name)}`);
|
|
1754
|
+
},
|
|
1755
|
+
updateMany: async (state, options = {}) => {
|
|
1756
|
+
return this.createOperationFromRequest("PUT", "/1.0/instances", {
|
|
1757
|
+
body: state,
|
|
1758
|
+
etag: options.etag
|
|
1759
|
+
});
|
|
1760
|
+
},
|
|
1761
|
+
rebuild: async (name, request) => {
|
|
1762
|
+
return this.createOperationFromRequest("POST", `/1.0/instances/${encodeURIComponent(name)}/rebuild`, { body: request });
|
|
1763
|
+
},
|
|
1764
|
+
rebuildFromImage: async (_source, _image, _name, _request) => createRemoteOperationFromTarget(null),
|
|
1765
|
+
state: async (name) => {
|
|
1766
|
+
const result = await this.requestEnvelope("GET", `/1.0/instances/${encodeURIComponent(name)}/state`);
|
|
1767
|
+
return { value: toRecord(result.value), etag: result.etag };
|
|
1768
|
+
},
|
|
1769
|
+
setState: async (name, state, options = {}) => {
|
|
1770
|
+
return this.createOperationFromRequest("PUT", `/1.0/instances/${encodeURIComponent(name)}/state`, {
|
|
1771
|
+
body: state,
|
|
1772
|
+
etag: options.etag
|
|
1773
|
+
});
|
|
1774
|
+
},
|
|
1775
|
+
access: async (name) => {
|
|
1776
|
+
const result = await this.requestEnvelope("GET", `/1.0/instances/${encodeURIComponent(name)}/access`);
|
|
1777
|
+
return toRecord(result.value);
|
|
1778
|
+
},
|
|
1779
|
+
exec: async (name, request, options) => {
|
|
1780
|
+
const path = `/1.0/instances/${encodeURIComponent(name)}/exec`;
|
|
1781
|
+
const body = { ...request };
|
|
1782
|
+
const shouldAttachIO = this.shouldAttachExecIO(options);
|
|
1783
|
+
if (shouldAttachIO && !hasWaitForWebsocketFlag(body)) {
|
|
1784
|
+
body["wait-for-websocket"] = true;
|
|
1785
|
+
}
|
|
1786
|
+
const result = await this.requestEnvelope("POST", path, {
|
|
1787
|
+
body,
|
|
1788
|
+
signal: options?.signal
|
|
1789
|
+
});
|
|
1790
|
+
const operation = this.createOperationFromEnvelopeResult("POST", path, result);
|
|
1791
|
+
if (shouldAttachIO) {
|
|
1792
|
+
await this.attachExecWebsockets(operation, body, options, result.value);
|
|
1793
|
+
}
|
|
1794
|
+
return operation;
|
|
1795
|
+
},
|
|
1796
|
+
console: async (name, request, options) => {
|
|
1797
|
+
return this.createOperationFromRequest("POST", `/1.0/instances/${encodeURIComponent(name)}/console`, {
|
|
1798
|
+
body: request,
|
|
1799
|
+
signal: options?.signal
|
|
1800
|
+
});
|
|
1801
|
+
},
|
|
1802
|
+
consoleDynamic: async () => {
|
|
1803
|
+
throw new Error("[Incus.ts] Dynamic console attach is not implemented yet");
|
|
1804
|
+
},
|
|
1805
|
+
metadata: async (name) => {
|
|
1806
|
+
const result = await this.requestEnvelope("GET", `/1.0/instances/${encodeURIComponent(name)}/metadata`);
|
|
1807
|
+
return { value: toRecord(result.value), etag: result.etag };
|
|
1808
|
+
},
|
|
1809
|
+
updateMetadata: async (name, metadata, options = {}) => {
|
|
1810
|
+
await this.requestEnvelope("PUT", `/1.0/instances/${encodeURIComponent(name)}/metadata`, {
|
|
1811
|
+
body: metadata,
|
|
1812
|
+
etag: options.etag
|
|
1813
|
+
});
|
|
1814
|
+
},
|
|
1815
|
+
debugMemory: async (name, format = "elf") => {
|
|
1816
|
+
const result = await this.requestBinary("GET", `/1.0/instances/${encodeURIComponent(name)}/debug/memory`, {
|
|
1817
|
+
query: {
|
|
1818
|
+
format,
|
|
1819
|
+
"instance-type": "virtual-machine"
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
return toReadableStream(result.value);
|
|
1823
|
+
},
|
|
1824
|
+
logs,
|
|
1825
|
+
files,
|
|
1826
|
+
templates: createNotImplementedProxy(["instances", "templates"]),
|
|
1827
|
+
snapshots: createNotImplementedProxy(["instances", "snapshots"]),
|
|
1828
|
+
backups: createNotImplementedProxy(["instances", "backups"])
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
class Incus {
|
|
1834
|
+
static async connect(endpoint, options = {}) {
|
|
1835
|
+
const normalized = normalizeEndpoint(endpoint);
|
|
1836
|
+
const descriptor = { kind: "incus", endpoint: normalized };
|
|
1837
|
+
return new IncusClient(new FetchTransport(descriptor, options), normalized, options, {}, "incus");
|
|
1838
|
+
}
|
|
1839
|
+
static async connectHttp(options = {}) {
|
|
1840
|
+
const endpoint = normalizeEndpoint(options.endpoint ?? "https://custom.socket");
|
|
1841
|
+
const descriptor = { kind: "incus-http", endpoint };
|
|
1842
|
+
return new IncusClient(new FetchTransport(descriptor, options), endpoint, options, {}, "incus-http");
|
|
1843
|
+
}
|
|
1844
|
+
static async connectUnix(options = {}) {
|
|
1845
|
+
const socketPath = options.socketPath ?? "/var/lib/incus/unix.socket";
|
|
1846
|
+
const endpoint = `unix://${socketPath}`;
|
|
1847
|
+
const descriptor = {
|
|
1848
|
+
kind: "incus-unix",
|
|
1849
|
+
endpoint,
|
|
1850
|
+
socketPath
|
|
1851
|
+
};
|
|
1852
|
+
return new IncusClient(new UnixSocketTransport(descriptor, options), endpoint, options, {}, "incus-unix");
|
|
1853
|
+
}
|
|
1854
|
+
static async connectPublic(endpoint, options = {}) {
|
|
1855
|
+
const normalized = normalizeEndpoint(endpoint);
|
|
1856
|
+
const descriptor = {
|
|
1857
|
+
kind: "incus-public",
|
|
1858
|
+
endpoint: normalized
|
|
1859
|
+
};
|
|
1860
|
+
return new IncusImageClient(new FetchTransport(descriptor, options), normalized, options, {}, "incus-public");
|
|
1861
|
+
}
|
|
1862
|
+
static async connectSimpleStreams(endpoint, options = {}) {
|
|
1863
|
+
const normalized = normalizeEndpoint(endpoint);
|
|
1864
|
+
const descriptor = {
|
|
1865
|
+
kind: "simple-streams",
|
|
1866
|
+
endpoint: normalized
|
|
1867
|
+
};
|
|
1868
|
+
return new IncusImageClient(new FetchTransport(descriptor, options), normalized, options, {}, "simple-streams");
|
|
1869
|
+
}
|
|
1870
|
+
static fromTransport(endpoint, transport, options = {}) {
|
|
1871
|
+
return new IncusClient(transport, normalizeEndpoint(endpoint), options);
|
|
1872
|
+
}
|
|
1873
|
+
static fromImageTransport(endpoint, transport, options = {}) {
|
|
1874
|
+
return new IncusImageClient(transport, normalizeEndpoint(endpoint), options);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
export {
|
|
1878
|
+
IncusImageClient,
|
|
1879
|
+
IncusClient,
|
|
1880
|
+
IncusApiError,
|
|
1881
|
+
Incus
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
//# debugId=841E8449BC239A1264756E2164756E21
|
|
1885
|
+
//# sourceMappingURL=index.js.map
|