@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.
Files changed (37) hide show
  1. package/assets/fonts/SourceSansPro-OFL.txt +46 -0
  2. package/assets/fonts/WorkSans-OFL.txt +46 -0
  3. package/package.json +4 -4
  4. package/runtime/fonts/OFL.txt +46 -0
  5. package/runtime/fonts/sourcesanspro-black.ttf +0 -0
  6. package/runtime/fonts/sourcesanspro-blackitalic.ttf +0 -0
  7. package/runtime/fonts/sourcesanspro-bold.ttf +0 -0
  8. package/runtime/fonts/sourcesanspro-bolditalic.ttf +0 -0
  9. package/runtime/fonts/sourcesanspro-extralight.ttf +0 -0
  10. package/runtime/fonts/sourcesanspro-extralightitalic.ttf +0 -0
  11. package/runtime/fonts/sourcesanspro-italic.ttf +0 -0
  12. package/runtime/fonts/sourcesanspro-light.ttf +0 -0
  13. package/runtime/fonts/sourcesanspro-lightitalic.ttf +0 -0
  14. package/runtime/fonts/sourcesanspro-regular.ttf +0 -0
  15. package/runtime/fonts/sourcesanspro-semibold.ttf +0 -0
  16. package/runtime/fonts/sourcesanspro-semibolditalic.ttf +0 -0
  17. package/runtime/manifest.json +7 -0
  18. package/runtime/render-wasm.js +15 -0
  19. package/runtime/render-wasm.wasm +0 -0
  20. package/runtime/renderer.mjs +169652 -0
  21. package/src/draft-import.mjs +1 -11
  22. package/src/font-convert.mjs +23 -1
  23. package/src/fs-utils.mjs +95 -0
  24. package/src/index.mjs +19 -3
  25. package/src/initialize.mjs +363 -136
  26. package/src/kiwi-runtime.mjs +38 -36
  27. package/src/library-network.mjs +189 -0
  28. package/src/local-package-backend.mjs +215 -24
  29. package/src/local-package.mjs +589 -177
  30. package/src/local-workspace-session.mjs +278 -96
  31. package/src/migrate-themes.mjs +116 -0
  32. package/src/remote-library.mjs +167 -28
  33. package/src/render-fonts.mjs +172 -0
  34. package/src/render-shared.mjs +346 -0
  35. package/src/render.mjs +71 -3557
  36. package/src/workspace.mjs +178 -17
  37. package/src/webp-decode.mjs +0 -73
@@ -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.readVarUint();
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.readVarUint();
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
- const data = this._data;
64
- let i = this._index;
65
- let b = data[i++];
66
- let value = b & 127;
67
- if (b < 128) {
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 < data.length && data[index] !== 0) index++;
130
- if (index >= data.length) throw new Error("Unterminated string in Kiwi message");
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.readVarUint();");
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.readVarUint();");
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.readVarUint();
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.readVarUint();
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
+ }