@neta-art/cohub-cli 3.10.2 → 3.12.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 +8 -4
- package/dist/board-command-support.d.ts +7 -0
- package/dist/board-command-support.js +99 -0
- package/dist/board-export.js +28 -22
- package/dist/commands/board-domain.d.ts +2 -0
- package/dist/commands/board-domain.js +8 -0
- package/dist/commands/boards/animation.d.ts +2 -0
- package/dist/commands/boards/animation.js +227 -0
- package/dist/commands/boards/appearance.d.ts +2 -0
- package/dist/commands/boards/appearance.js +93 -0
- package/dist/commands/boards/context.d.ts +12 -0
- package/dist/commands/boards/context.js +25 -0
- package/dist/commands/boards/nodes.d.ts +2 -0
- package/dist/commands/boards/nodes.js +110 -0
- package/dist/commands/boards.d.ts +3 -2
- package/dist/commands/boards.js +126 -57
- package/dist/commands/ui.js +72 -13
- package/dist/safe-remote-image.d.ts +24 -0
- package/dist/safe-remote-image.js +160 -0
- package/package.json +2 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import dns from "node:dns/promises";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import { isIP } from "node:net";
|
|
5
|
+
import { isPublicBoardRemoteAddress, normalizeBoardRemoteUrl, } from "@neta-art/cohub/board";
|
|
6
|
+
export const REMOTE_IMAGE_MAX_BYTES = 16 * 1024 * 1024;
|
|
7
|
+
export const REMOTE_IMAGE_TIMEOUT_MS = 15_000;
|
|
8
|
+
const MAX_REDIRECTS = 3;
|
|
9
|
+
const IMAGE_MIME_TYPES = new Set([
|
|
10
|
+
"image/avif",
|
|
11
|
+
"image/gif",
|
|
12
|
+
"image/jpeg",
|
|
13
|
+
"image/png",
|
|
14
|
+
"image/webp",
|
|
15
|
+
]);
|
|
16
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
17
|
+
async function defaultLookup(hostname) {
|
|
18
|
+
const records = await dns.lookup(hostname, { all: true, verbatim: true });
|
|
19
|
+
return records.map((record) => ({
|
|
20
|
+
address: record.address,
|
|
21
|
+
family: record.family,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
function responseHeaders(input) {
|
|
25
|
+
const headers = new Headers();
|
|
26
|
+
for (const [key, value] of Object.entries(input)) {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
for (const item of value)
|
|
29
|
+
headers.append(key, item);
|
|
30
|
+
}
|
|
31
|
+
else if (value !== undefined) {
|
|
32
|
+
headers.set(key, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return headers;
|
|
36
|
+
}
|
|
37
|
+
function requestPinned(url, address, timeoutMs, maxBytes) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const client = url.protocol === "https:" ? https : http;
|
|
40
|
+
const request = client.request(url, {
|
|
41
|
+
agent: false,
|
|
42
|
+
headers: { Accept: "image/avif,image/webp,image/png,image/jpeg,image/gif" },
|
|
43
|
+
lookup: (_hostname, _options, callback) => {
|
|
44
|
+
callback(null, address.address, address.family);
|
|
45
|
+
},
|
|
46
|
+
}, (response) => {
|
|
47
|
+
const status = response.statusCode ?? 0;
|
|
48
|
+
const headers = responseHeaders(response.headers);
|
|
49
|
+
if (REDIRECT_STATUSES.has(status)) {
|
|
50
|
+
response.resume();
|
|
51
|
+
resolve({ status, headers, bytes: new Uint8Array() });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const declaredLength = Number(headers.get("content-length") ?? 0);
|
|
55
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
56
|
+
response.destroy();
|
|
57
|
+
reject(new Error(`Image exceeds the ${maxBytes} byte download limit`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let total = 0;
|
|
62
|
+
response.on("data", (chunk) => {
|
|
63
|
+
total += chunk.byteLength;
|
|
64
|
+
if (total > maxBytes) {
|
|
65
|
+
response.destroy(new Error(`Image exceeds the ${maxBytes} byte download limit`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
chunks.push(chunk);
|
|
69
|
+
});
|
|
70
|
+
response.once("error", reject);
|
|
71
|
+
response.once("end", () => {
|
|
72
|
+
const bytes = new Uint8Array(total);
|
|
73
|
+
let offset = 0;
|
|
74
|
+
for (const chunk of chunks) {
|
|
75
|
+
bytes.set(chunk, offset);
|
|
76
|
+
offset += chunk.byteLength;
|
|
77
|
+
}
|
|
78
|
+
resolve({ status, headers, bytes });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
request.setTimeout(timeoutMs, () => {
|
|
82
|
+
request.destroy(new Error(`Image download timed out after ${timeoutMs}ms`));
|
|
83
|
+
});
|
|
84
|
+
request.once("error", reject);
|
|
85
|
+
request.end();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function remainingMs(deadline) {
|
|
89
|
+
const remaining = deadline - Date.now();
|
|
90
|
+
if (remaining <= 0)
|
|
91
|
+
throw new Error("Image download timed out");
|
|
92
|
+
return remaining;
|
|
93
|
+
}
|
|
94
|
+
async function withDeadline(promise, deadline) {
|
|
95
|
+
const timeoutMs = remainingMs(deadline);
|
|
96
|
+
let timer;
|
|
97
|
+
try {
|
|
98
|
+
return await Promise.race([
|
|
99
|
+
promise,
|
|
100
|
+
new Promise((_, reject) => {
|
|
101
|
+
timer = setTimeout(() => reject(new Error(`Image download timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
102
|
+
}),
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (timer)
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function resolvePublicUrl(value, lookup, deadline) {
|
|
111
|
+
const normalized = normalizeBoardRemoteUrl(value);
|
|
112
|
+
if (!normalized)
|
|
113
|
+
throw new Error("Image URL must be a public HTTP(S) URL");
|
|
114
|
+
const url = new URL(normalized);
|
|
115
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
116
|
+
const addresses = isIP(hostname)
|
|
117
|
+
? [{ address: hostname, family: isIP(hostname) }]
|
|
118
|
+
: await withDeadline(lookup(hostname), deadline);
|
|
119
|
+
if (addresses.length === 0 ||
|
|
120
|
+
addresses.some((entry) => !isPublicBoardRemoteAddress(entry.address))) {
|
|
121
|
+
throw new Error("Image URL resolves to a private address");
|
|
122
|
+
}
|
|
123
|
+
return { url, address: addresses[0] };
|
|
124
|
+
}
|
|
125
|
+
export async function downloadPublicImage(input, options = {}) {
|
|
126
|
+
const lookup = options.lookup ?? defaultLookup;
|
|
127
|
+
const requester = options.requester ?? requestPinned;
|
|
128
|
+
const maxBytes = options.maxBytes ?? REMOTE_IMAGE_MAX_BYTES;
|
|
129
|
+
const deadline = Date.now() + (options.timeoutMs ?? REMOTE_IMAGE_TIMEOUT_MS);
|
|
130
|
+
let current = input;
|
|
131
|
+
for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect += 1) {
|
|
132
|
+
const { url, address } = await resolvePublicUrl(current, lookup, deadline);
|
|
133
|
+
const response = await withDeadline(requester(url, address, remainingMs(deadline), maxBytes), deadline);
|
|
134
|
+
if (REDIRECT_STATUSES.has(response.status)) {
|
|
135
|
+
const location = response.headers.get("location");
|
|
136
|
+
if (!location)
|
|
137
|
+
throw new Error("Image redirect is missing a location");
|
|
138
|
+
if (redirect === MAX_REDIRECTS)
|
|
139
|
+
throw new Error("Too many image redirects");
|
|
140
|
+
current = new URL(location, url).toString();
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (response.status < 200 || response.status >= 300) {
|
|
144
|
+
throw new Error(`HTTP ${response.status}`);
|
|
145
|
+
}
|
|
146
|
+
if (response.bytes.byteLength > maxBytes) {
|
|
147
|
+
throw new Error(`Image exceeds the ${maxBytes} byte download limit`);
|
|
148
|
+
}
|
|
149
|
+
const mimeType = response.headers
|
|
150
|
+
.get("content-type")
|
|
151
|
+
?.split(";", 1)[0]
|
|
152
|
+
?.trim()
|
|
153
|
+
.toLowerCase();
|
|
154
|
+
if (!mimeType || !IMAGE_MIME_TYPES.has(mimeType)) {
|
|
155
|
+
throw new Error("Remote background must be a supported raster image");
|
|
156
|
+
}
|
|
157
|
+
return { bytes: response.bytes, mimeType };
|
|
158
|
+
}
|
|
159
|
+
throw new Error("Too many image redirects");
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"commander": "^15.0.0",
|
|
20
20
|
"pixi.js": "^8.19.0",
|
|
21
21
|
"sharp": "^0.35.3",
|
|
22
|
-
"@neta-art/cohub": "5.
|
|
22
|
+
"@neta-art/cohub": "5.10.0"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|