@smallpen/local-package 0.1.0-alpha.1 → 0.1.0-alpha.11
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/assets/fonts/SourceSansPro-OFL.txt +46 -0
- package/assets/fonts/WorkSans-OFL.txt +46 -0
- package/package.json +4 -4
- package/runtime/fonts/OFL.txt +46 -0
- package/runtime/fonts/sourcesanspro-black.ttf +0 -0
- package/runtime/fonts/sourcesanspro-blackitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-bold.ttf +0 -0
- package/runtime/fonts/sourcesanspro-bolditalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-extralight.ttf +0 -0
- package/runtime/fonts/sourcesanspro-extralightitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-italic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-light.ttf +0 -0
- package/runtime/fonts/sourcesanspro-lightitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-regular.ttf +0 -0
- package/runtime/fonts/sourcesanspro-semibold.ttf +0 -0
- package/runtime/fonts/sourcesanspro-semibolditalic.ttf +0 -0
- package/runtime/manifest.json +7 -0
- package/runtime/render-wasm.js +15 -0
- package/runtime/render-wasm.wasm +0 -0
- package/runtime/renderer.mjs +169652 -0
- package/src/draft-import.mjs +1 -11
- package/src/font-convert.mjs +23 -1
- package/src/fs-utils.mjs +95 -0
- package/src/index.mjs +19 -3
- package/src/initialize.mjs +363 -136
- package/src/kiwi-runtime.mjs +38 -36
- package/src/library-network.mjs +189 -0
- package/src/local-package-backend.mjs +215 -24
- package/src/local-package.mjs +589 -177
- package/src/local-workspace-session.mjs +278 -96
- package/src/migrate-themes.mjs +116 -0
- package/src/remote-library.mjs +167 -28
- package/src/render-fonts.mjs +172 -0
- package/src/render-shared.mjs +346 -0
- package/src/render.mjs +71 -3557
- package/src/workspace.mjs +178 -17
- package/src/webp-decode.mjs +0 -73
package/src/kiwi-runtime.mjs
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
let int32 = /* @__PURE__ */ new Int32Array(1);
|
|
6
6
|
let float32 = new Float32Array(int32.buffer);
|
|
7
7
|
const textDecoder = new TextDecoder();
|
|
8
|
+
// Struct fields carry no tag, so a struct with no fields, or a tree of struct
|
|
9
|
+
// fields, decodes to objects without consuming any bytes. Every decoded struct,
|
|
10
|
+
// message, and array element counts against this budget instead.
|
|
11
|
+
const MAX_DECODED_VALUES = 5e6;
|
|
8
12
|
var ByteBuffer = class {
|
|
9
13
|
_data;
|
|
14
|
+
_decodedValues = 0;
|
|
10
15
|
_index;
|
|
11
16
|
length;
|
|
12
17
|
constructor(data) {
|
|
@@ -32,20 +37,36 @@ var ByteBuffer = class {
|
|
|
32
37
|
toUint8Array() {
|
|
33
38
|
return this._data.subarray(0, this.length);
|
|
34
39
|
}
|
|
40
|
+
// SmallPen hardening: clipboard payloads are untrusted, so every read is
|
|
41
|
+
// bounds-checked and declared lengths may not exceed the remaining bytes.
|
|
42
|
+
_require(count) {
|
|
43
|
+
if (this._index + count > this.length) throw new Error("Kiwi message ends before the declared data");
|
|
44
|
+
}
|
|
45
|
+
countDecodedValues(count) {
|
|
46
|
+
this._decodedValues += count;
|
|
47
|
+
if (this._decodedValues > MAX_DECODED_VALUES) throw new Error("Kiwi message decodes to too many values");
|
|
48
|
+
}
|
|
49
|
+
readArrayLength() {
|
|
50
|
+
const length = this.readVarUint();
|
|
51
|
+
if (length > this.length - this._index) throw new Error("Kiwi array length exceeds the message");
|
|
52
|
+
return length;
|
|
53
|
+
}
|
|
35
54
|
readByte() {
|
|
55
|
+
this._require(1);
|
|
36
56
|
return this._data[this._index++];
|
|
37
57
|
}
|
|
38
58
|
readByteArray() {
|
|
39
|
-
const length = this.
|
|
59
|
+
const length = this.readArrayLength();
|
|
40
60
|
const start = this._index;
|
|
41
61
|
this._index = start + length;
|
|
42
62
|
return this._data.slice(start, start + length);
|
|
43
63
|
}
|
|
44
64
|
skipByteArray() {
|
|
45
|
-
const length = this.
|
|
65
|
+
const length = this.readArrayLength();
|
|
46
66
|
this._index += length;
|
|
47
67
|
}
|
|
48
68
|
readVarFloat() {
|
|
69
|
+
this._require(1);
|
|
49
70
|
const index = this._index;
|
|
50
71
|
const data = this._data;
|
|
51
72
|
const first = data[index];
|
|
@@ -53,6 +74,7 @@ var ByteBuffer = class {
|
|
|
53
74
|
this._index = index + 1;
|
|
54
75
|
return 0;
|
|
55
76
|
}
|
|
77
|
+
this._require(4);
|
|
56
78
|
let bits = first | data[index + 1] << 8 | data[index + 2] << 16 | data[index + 3] << 24;
|
|
57
79
|
this._index = index + 4;
|
|
58
80
|
bits = bits << 23 | bits >>> 9;
|
|
@@ -60,35 +82,12 @@ var ByteBuffer = class {
|
|
|
60
82
|
return float32[0];
|
|
61
83
|
}
|
|
62
84
|
readVarUint() {
|
|
63
|
-
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this._index = i;
|
|
69
|
-
return value;
|
|
70
|
-
}
|
|
71
|
-
b = data[i++];
|
|
72
|
-
value |= (b & 127) << 7;
|
|
73
|
-
if (b < 128) {
|
|
74
|
-
this._index = i;
|
|
75
|
-
return value;
|
|
76
|
-
}
|
|
77
|
-
b = data[i++];
|
|
78
|
-
value |= (b & 127) << 14;
|
|
79
|
-
if (b < 128) {
|
|
80
|
-
this._index = i;
|
|
81
|
-
return value;
|
|
82
|
-
}
|
|
83
|
-
b = data[i++];
|
|
84
|
-
value |= (b & 127) << 21;
|
|
85
|
-
if (b < 128) {
|
|
86
|
-
this._index = i;
|
|
87
|
-
return value;
|
|
85
|
+
let value = 0;
|
|
86
|
+
for (let shift = 0; shift < 35; shift += 7) {
|
|
87
|
+
const b = this.readByte();
|
|
88
|
+
value |= (b & 127) << shift;
|
|
89
|
+
if (b < 128) return shift === 28 ? value >>> 0 : value;
|
|
88
90
|
}
|
|
89
|
-
b = data[i++];
|
|
90
|
-
value |= (b & 127) << 28;
|
|
91
|
-
this._index = i;
|
|
92
91
|
return value >>> 0;
|
|
93
92
|
}
|
|
94
93
|
readVarInt() {
|
|
@@ -126,8 +125,8 @@ var ByteBuffer = class {
|
|
|
126
125
|
findStringTerminator(start) {
|
|
127
126
|
const data = this._data;
|
|
128
127
|
let index = start;
|
|
129
|
-
while (index <
|
|
130
|
-
if (index >=
|
|
128
|
+
while (index < this.length && data[index] !== 0) index++;
|
|
129
|
+
if (index >= this.length) throw new Error("Unterminated string in Kiwi message");
|
|
131
130
|
return index;
|
|
132
131
|
}
|
|
133
132
|
_growBy(amount) {
|
|
@@ -244,6 +243,7 @@ function compileDecode(definition, definitions) {
|
|
|
244
243
|
lines.push(" if (!(bb instanceof this.ByteBuffer)) {");
|
|
245
244
|
lines.push(" bb = new this.ByteBuffer(bb);");
|
|
246
245
|
lines.push(" }");
|
|
246
|
+
lines.push(" bb.countDecodedValues(1);");
|
|
247
247
|
lines.push("");
|
|
248
248
|
if (definition.kind === "MESSAGE") {
|
|
249
249
|
lines.push(" while (true) {");
|
|
@@ -293,12 +293,14 @@ function compileDecode(definition, definitions) {
|
|
|
293
293
|
if (field.isDeprecated) {
|
|
294
294
|
if (field.type === "byte") lines.push(indent + "bb.readByteArray();");
|
|
295
295
|
else {
|
|
296
|
-
lines.push(indent + "var length = bb.
|
|
296
|
+
lines.push(indent + "var length = bb.readArrayLength();");
|
|
297
|
+
lines.push(indent + "bb.countDecodedValues(length);");
|
|
297
298
|
lines.push(indent + "while (length-- > 0) " + code + ";");
|
|
298
299
|
}
|
|
299
300
|
} else if (field.type === "byte") lines.push(indent + "result[" + quote(field.name) + "] = bb.readByteArray();");
|
|
300
301
|
else {
|
|
301
|
-
lines.push(indent + "var length = bb.
|
|
302
|
+
lines.push(indent + "var length = bb.readArrayLength();");
|
|
303
|
+
lines.push(indent + "bb.countDecodedValues(length);");
|
|
302
304
|
lines.push(indent + "var values = result[" + quote(field.name) + "] = Array(length);");
|
|
303
305
|
lines.push(indent + "for (var i = 0; i < length; i++) values[i] = " + code + ";");
|
|
304
306
|
}
|
|
@@ -450,12 +452,12 @@ let kinds = [
|
|
|
450
452
|
];
|
|
451
453
|
function decodeBinarySchema(buffer) {
|
|
452
454
|
let bb = buffer instanceof ByteBuffer ? buffer : new ByteBuffer(buffer);
|
|
453
|
-
let definitionCount = bb.
|
|
455
|
+
let definitionCount = bb.readArrayLength();
|
|
454
456
|
let definitions = [];
|
|
455
457
|
for (let i = 0; i < definitionCount; i++) {
|
|
456
458
|
let definitionName = bb.readString();
|
|
457
459
|
let kind = bb.readByte();
|
|
458
|
-
let fieldCount = bb.
|
|
460
|
+
let fieldCount = bb.readArrayLength();
|
|
459
461
|
let fields = [];
|
|
460
462
|
for (let j = 0; j < fieldCount; j++) {
|
|
461
463
|
let fieldName = bb.readString();
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from "node:dns";
|
|
2
|
+
import { Agent as HttpAgent, request as httpRequest } from "node:http";
|
|
3
|
+
import { Agent as HttpsAgent, request as httpsRequest } from "node:https";
|
|
4
|
+
import { BlockList, isIP } from "node:net";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
|
|
7
|
+
import { SmallPenError } from "@smallpen/core";
|
|
8
|
+
|
|
9
|
+
// A Package can declare any Library URL, so by default the Background and CLI
|
|
10
|
+
// only fetch Libraries from public addresses: a Package must not be able to
|
|
11
|
+
// make them probe loopback services, cloud metadata endpoints or the LAN.
|
|
12
|
+
// Local development servers opt in with this variable (or the
|
|
13
|
+
// `allowPrivateNetwork` option).
|
|
14
|
+
export const ALLOW_PRIVATE_LIBRARY_HOSTS_ENV = "SMALLPEN_ALLOW_PRIVATE_LIBRARY_HOSTS";
|
|
15
|
+
|
|
16
|
+
export function privateLibraryHostsAllowed() {
|
|
17
|
+
return process.env[ALLOW_PRIVATE_LIBRARY_HOSTS_ENV] === "1";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 198.18.0.0/15 stays allowed: proxy tools in fake-IP mode (Clash, Surge)
|
|
21
|
+
// answer every public name with an address from that range.
|
|
22
|
+
const nonPublic = new BlockList();
|
|
23
|
+
for (const [network, prefix] of [
|
|
24
|
+
["0.0.0.0", 8],
|
|
25
|
+
["10.0.0.0", 8],
|
|
26
|
+
["100.64.0.0", 10],
|
|
27
|
+
["127.0.0.0", 8],
|
|
28
|
+
["169.254.0.0", 16],
|
|
29
|
+
["172.16.0.0", 12],
|
|
30
|
+
["192.0.0.0", 24],
|
|
31
|
+
["192.0.2.0", 24],
|
|
32
|
+
["192.168.0.0", 16],
|
|
33
|
+
["198.51.100.0", 24],
|
|
34
|
+
["203.0.113.0", 24],
|
|
35
|
+
["224.0.0.0", 4],
|
|
36
|
+
["240.0.0.0", 4],
|
|
37
|
+
]) {
|
|
38
|
+
nonPublic.addSubnet(network, prefix, "ipv4");
|
|
39
|
+
}
|
|
40
|
+
for (const [network, prefix] of [
|
|
41
|
+
["::", 96],
|
|
42
|
+
["::1", 128],
|
|
43
|
+
["100::", 64],
|
|
44
|
+
["2001:db8::", 32],
|
|
45
|
+
["fc00::", 7],
|
|
46
|
+
["fe80::", 10],
|
|
47
|
+
["fec0::", 10],
|
|
48
|
+
["ff00::", 8],
|
|
49
|
+
]) {
|
|
50
|
+
nonPublic.addSubnet(network, prefix, "ipv6");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ipv6Groups(address) {
|
|
54
|
+
let text = address.replace(/%.*$/, "").toLowerCase();
|
|
55
|
+
const dotted = /(\d+\.\d+\.\d+\.\d+)$/.exec(text);
|
|
56
|
+
if (dotted) {
|
|
57
|
+
const [a, b, c, d] = dotted[1].split(".").map(Number);
|
|
58
|
+
text = `${text.slice(0, -dotted[1].length)}${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;
|
|
59
|
+
}
|
|
60
|
+
const [head, tail] = text.split("::");
|
|
61
|
+
const left = head ? head.split(":") : [];
|
|
62
|
+
const right = tail ? tail.split(":") : [];
|
|
63
|
+
const fill = tail === undefined ? [] : Array(8 - left.length - right.length).fill("0");
|
|
64
|
+
return [...left, ...fill, ...right].map((group) => Number.parseInt(group, 16));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// NAT64 (64:ff9b::/96) and 6to4 (2002::/16) addresses carry an IPv4 address
|
|
68
|
+
// that the network routes to; check that address too.
|
|
69
|
+
function embeddedIPv4(address) {
|
|
70
|
+
const groups = ipv6Groups(address);
|
|
71
|
+
const hi = (value) => value >> 8;
|
|
72
|
+
const lo = (value) => value & 0xff;
|
|
73
|
+
let pair;
|
|
74
|
+
if (groups[0] === 0x64 && groups[1] === 0xff9b && groups.slice(2, 6).every((g) => g === 0)) {
|
|
75
|
+
pair = [groups[6], groups[7]];
|
|
76
|
+
} else if (groups[0] === 0x2002) {
|
|
77
|
+
pair = [groups[1], groups[2]];
|
|
78
|
+
}
|
|
79
|
+
return pair && `${hi(pair[0])}.${lo(pair[0])}.${hi(pair[1])}.${lo(pair[1])}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isPublicAddress(address) {
|
|
83
|
+
const family = isIP(address.replace(/%.*$/, ""));
|
|
84
|
+
if (family === 4) return !nonPublic.check(address, "ipv4");
|
|
85
|
+
if (family !== 6) return false;
|
|
86
|
+
if (nonPublic.check(address, "ipv6")) return false;
|
|
87
|
+
const embedded = embeddedIPv4(address);
|
|
88
|
+
return embedded === undefined || !nonPublic.check(embedded, "ipv4");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function privateAddressError(url) {
|
|
92
|
+
return new SmallPenError(
|
|
93
|
+
"remote_library_private_address",
|
|
94
|
+
"Remote Library host is not on a public network address",
|
|
95
|
+
{ env: ALLOW_PRIVATE_LIBRARY_HOSTS_ENV, url: String(url) },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Every resolved address must be public, and the socket connects to the
|
|
100
|
+
// address checked here: there is no second resolution a rebinding DNS server
|
|
101
|
+
// could answer differently.
|
|
102
|
+
function publicLookup(url, allowPrivateNetwork) {
|
|
103
|
+
return (hostname, options, callback) => {
|
|
104
|
+
dnsLookup(hostname, { ...options, all: true }, (error, addresses) => {
|
|
105
|
+
if (error) {
|
|
106
|
+
callback(error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
!allowPrivateNetwork &&
|
|
111
|
+
(addresses.length === 0 ||
|
|
112
|
+
!addresses.every(({ address }) => isPublicAddress(address)))
|
|
113
|
+
) {
|
|
114
|
+
callback(privateAddressError(url));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (options.all) callback(null, addresses);
|
|
118
|
+
else callback(null, addresses[0].address, addresses[0].family);
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
|
|
124
|
+
|
|
125
|
+
// Dedicated agents per policy: a pooled socket opened without the address
|
|
126
|
+
// check (by another request in this process, or under the private-network
|
|
127
|
+
// opt-in) is never reused for a checked Library fetch.
|
|
128
|
+
const agentPools = new Map(
|
|
129
|
+
[false, true].map((allowPrivateNetwork) => [
|
|
130
|
+
allowPrivateNetwork,
|
|
131
|
+
{
|
|
132
|
+
"http:": new HttpAgent({ keepAlive: true }),
|
|
133
|
+
"https:": new HttpsAgent({ keepAlive: true }),
|
|
134
|
+
},
|
|
135
|
+
]),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// A minimal fetch() over node:http(s) that refuses non-public destinations
|
|
139
|
+
// unless `allowPrivateNetwork` is set. It supports what remote Library reads
|
|
140
|
+
// use: GET, manual redirects and an abort signal.
|
|
141
|
+
export function createLibraryFetch({
|
|
142
|
+
allowPrivateNetwork = privateLibraryHostsAllowed(),
|
|
143
|
+
} = {}) {
|
|
144
|
+
return (input, init) =>
|
|
145
|
+
libraryFetch(input, init, allowPrivateNetwork === true);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function libraryFetch(input, { signal } = {}, allowPrivateNetwork) {
|
|
149
|
+
const url = new URL(input);
|
|
150
|
+
const literal = url.hostname.replace(/^\[|\]$/g, "");
|
|
151
|
+
if (!allowPrivateNetwork && isIP(literal) && !isPublicAddress(literal)) {
|
|
152
|
+
return Promise.reject(privateAddressError(url));
|
|
153
|
+
}
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(
|
|
156
|
+
url,
|
|
157
|
+
{
|
|
158
|
+
agent: agentPools.get(allowPrivateNetwork)[url.protocol],
|
|
159
|
+
headers: { accept: "application/json, */*" },
|
|
160
|
+
lookup: publicLookup(url, allowPrivateNetwork),
|
|
161
|
+
method: "GET",
|
|
162
|
+
signal,
|
|
163
|
+
},
|
|
164
|
+
(message) => {
|
|
165
|
+
try {
|
|
166
|
+
const headers = new Headers();
|
|
167
|
+
for (const [name, value] of Object.entries(message.headers)) {
|
|
168
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
169
|
+
headers.append(name, item);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const nullBody = NULL_BODY_STATUSES.has(message.statusCode);
|
|
173
|
+
if (nullBody) message.resume();
|
|
174
|
+
resolve(
|
|
175
|
+
new Response(nullBody ? null : Readable.toWeb(message), {
|
|
176
|
+
headers,
|
|
177
|
+
status: message.statusCode,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
message.destroy();
|
|
182
|
+
reject(error);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
request.on("error", reject);
|
|
187
|
+
request.end();
|
|
188
|
+
});
|
|
189
|
+
}
|