@threadedstack/wmd-web 0.1.17 → 0.1.19

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 CHANGED
@@ -155,6 +155,50 @@ The interface this host implements is in [docs/host-interface.md](docs/host-inte
155
155
  too) and shows the last 128 KB. P-B1's merged branch had the same rings at 4 MiB
156
156
  (cherry-picked as `17e9ce50`); the merge keeps P-B2's 32 MiB rings and drop markers.
157
157
 
158
+ ## Building images (`wmd-web/build`)
159
+
160
+ `createBuilder` runs BuildKit (moby/buildkit v0.33.0) in a privileged builder machine in the page and builds a
161
+ Dockerfile there. The result is `{index, packs}`, the shape `pullImage` returns, so `bootWmd` can run it at once.
162
+ Chromium, Firefox and WebKit all pass the same 18 build steps (plan `docs/plans/web-buildkit-impl.md`, section 10).
163
+
164
+ ```js
165
+ import { createBuilder } from 'wmd-web/build'
166
+
167
+ const builder = await createBuilder({
168
+ vmUrl, files, // as bootWmd: the engine and the builder arch's guest files
169
+ arch: 'arm64', // or 'amd64'
170
+ image: { tar: builderTarUrl, sha256 }, // the builder image; must match BUILDER_PINS in src/build/pin.js
171
+ relay: { url, protocol: 'wisp' }, // the guest's network: base images and RUN downloads
172
+ disk: { namespace: 'my-app' }, // BuildKit's cache on an ext4 disk in OPFS (default 10240 MiB)
173
+ converter, // wmdpack.wasm
174
+ pull: ref => pullImage(ref, { fetch, converter, platform: 'linux/arm64' }), // base images for the fast path (off without it)
175
+ })
176
+ const r = await builder.build({ context: { Dockerfile: 'FROM alpine:3.20\nRUN echo hi > /x\n' } })
177
+ // r: {index, packs, log, path: 'fast'|'buildkit', ms, cached}
178
+ await builder.close()
179
+ ```
180
+
181
+ - **The cache stays.** The disk outlives the builder and the visit. After a reload, or after a tab was killed
182
+ mid-build, the next build mounts the disk again and its steps come back `CACHED`. `close({keep: false})` deletes
183
+ the disk.
184
+ - **A second tab** that asks for a disk already open gets a temporary disk, deleted when it closes.
185
+ - **Features:** `RUN --mount=type=cache` and `type=secret`, `# syntax=` frontends, build args and target stages,
186
+ `.dockerignore`, file capabilities (`setcap`), push to a registry (TLS, or plain http in `httpRegistries`), and
187
+ `tar: true` for a Docker tar that `docker load` takes. `--mount=type=ssh` and another platform are refused before
188
+ any work.
189
+ - **The fast path.** With `pull` given, a Dockerfile with no `RUN` is built in the page without BuildKit, layer
190
+ for layer the same as BuildKit's (40 compared cases): under 0.4 s once its base image is pulled, where a warm
191
+ BuildKit takes 5 to 14 s.
192
+ - **Errors:** `BuildError {step, tail, reason}`: `reason` is `'no-space'`, `'quota'`, `'disk'`, `'timeout'`, `'pin'`
193
+ or `'secret'`. Cancelling with `signal` stops the step and leaves nothing behind.
194
+ - **The builder images** are built with `make web-builder` (wmd builds them; Docker Desktop is not used) into
195
+ `web/dist/builder/builder-<arch>.tar`, and their sha256 is pinned in `src/build/pin.js`. `make web-release` puts
196
+ them next to the release tgz.
197
+ - **Docker Hub's rate limit** applies to base images. `registryMirrors` gives BuildKit a mirror to try first.
198
+ - **Tests:** `bash ../scripts/bootlock.sh run --max 1800 -- bunx playwright test -c test/build --project=<browser>`
199
+ (`build.spec.js`: the 18 steps; `parity.spec.js`: the fast path against BuildKit). `test/build/seed-mirror.sh`
200
+ seeds a local mirror so the runs never touch Docker Hub.
201
+
158
202
  ## Running the tests
159
203
 
160
204
  ```sh
package/RELEASE.json CHANGED
@@ -1,38 +1,9 @@
1
1
  {
2
2
  "name": "wmd-web",
3
- "version": "0.3.2",
4
- "commit": "fa2b21cfd0fd0bf679498e6feeefc26c33f17372",
5
- "previous": null,
3
+ "version": "0.4.0",
4
+ "commit": "89688f12e0caef39ab0a159b39c00f3525e31051",
5
+ "previous": "wmd-web-v0.1.0",
6
6
  "decisions": [
7
- "D-388",
8
- "D-395",
9
- "D-408",
10
- "D-415",
11
- "D-416",
12
- "D-417",
13
- "D-419",
14
- "D-423",
15
- "D-428",
16
- "D-430",
17
- "D-432",
18
- "D-433",
19
- "D-438",
20
- "D-442",
21
- "D-443",
22
- "D-444",
23
- "D-445",
24
- "D-446",
25
- "D-447",
26
- "D-448",
27
- "D-449",
28
- "D-451",
29
- "D-453",
30
- "D-454",
31
- "D-468",
32
- "D-476",
33
- "D-477",
34
- "D-481",
35
- "D-482",
36
7
  "D-486",
37
8
  "D-488",
38
9
  "D-494",
@@ -49,43 +20,53 @@
49
20
  "D-539",
50
21
  "D-542",
51
22
  "D-544",
52
- "D-545"
23
+ "D-545",
24
+ "D-547",
25
+ "D-548"
53
26
  ],
54
- "vmSources": "c7f2793941170d96db37d9a12a8a5ad072fda7de",
27
+ "vmSources": "7a707b82c43d3f1eb999476cad68e7ce79cc9cb5",
55
28
  "guest": {
56
29
  "arch": "arm64",
57
30
  "files": {
58
31
  "vm.wasm": {
59
- "sha256": "2504f4071537e205d78766f2c84219fe49ea4709314becff3a314765733e7fdf",
60
- "bytes": 1561314
32
+ "sha256": "40299e37e788189e6d3bea9d9cdaefa241a5acf27931e6aae14117c100b036ec",
33
+ "bytes": 1561499
61
34
  },
62
35
  "Image": {
63
- "sha256": "a6b3209cf69d5bc1dacf0f3ed67f92c535dbb4108d67b47091c2aef87c6d7b04",
64
- "bytes": 39047176
36
+ "sha256": "1d296c86b1fee0a10d0c2571b7e19854fe85ca0250cc9fadfe75eb268e48be7b",
37
+ "bytes": 39049224
65
38
  },
66
39
  "Image-4k": {
67
- "sha256": "8ff6e338a0b42d9f774b35423e0e7f9d7f779c635446e8de2a3beb83d3518f8a",
40
+ "sha256": "6296e3d9fc7bb06a26340bb4453b5f0970d6b55452903546ba2c762820e2df5b",
68
41
  "bytes": 38828040
69
42
  },
70
43
  "initramfs.cpio.gz": {
71
- "sha256": "a2260db90e5d8acdf12a2123f696b4512a1465856d5653e20a85504069a3480c",
72
- "bytes": 4685323
44
+ "sha256": "f008aa9e0f6b4e913242f9982d3b194dba7dc39042a24bc89be64630672125af",
45
+ "bytes": 4685204
73
46
  }
74
47
  }
75
48
  },
76
49
  "files": {
77
- "README.md": "6873b42beaf44e02eded67fa54aaa2c13d766597048fbc8633909b6d77b049e0",
50
+ "README.md": "3c12ca29aab5160b955ba3f50035f19be1dbf0278c4fb006e253a37ac9efbe6a",
78
51
  "export-page/coi-sw.js": "0300e670b413cb2e0aae35843fc583546bbf6687df971ab6df41cfa3b0d494d7",
79
52
  "export-page/index.html": "f5450c89e63601bbbbffef421f03491b231d1f143070012c1c5315d4b0059f43",
80
53
  "export-page/run.css": "b57adf1e76553549c1f89ff157b7973409dedc21889cd3932948cd05b51adc6f",
81
54
  "export-page/run.js": "717636a632a40469106ddf1383e198eaac5d83651b5866d0699ec04b82462b7f",
82
- "guest/arm64/Image": "a6b3209cf69d5bc1dacf0f3ed67f92c535dbb4108d67b47091c2aef87c6d7b04",
83
- "guest/arm64/Image-4k": "8ff6e338a0b42d9f774b35423e0e7f9d7f779c635446e8de2a3beb83d3518f8a",
84
- "guest/arm64/initramfs.cpio.gz": "a2260db90e5d8acdf12a2123f696b4512a1465856d5653e20a85504069a3480c",
85
- "guest/arm64/vm.wasm": "2504f4071537e205d78766f2c84219fe49ea4709314becff3a314765733e7fdf",
86
- "package.json": "17aa3a8406e8d2d85b647c426438d3fd75edcb59d83e257882227e7b93cccbc0",
55
+ "guest/arm64/Image": "1d296c86b1fee0a10d0c2571b7e19854fe85ca0250cc9fadfe75eb268e48be7b",
56
+ "guest/arm64/Image-4k": "6296e3d9fc7bb06a26340bb4453b5f0970d6b55452903546ba2c762820e2df5b",
57
+ "guest/arm64/initramfs.cpio.gz": "f008aa9e0f6b4e913242f9982d3b194dba7dc39042a24bc89be64630672125af",
58
+ "guest/arm64/vm.wasm": "40299e37e788189e6d3bea9d9cdaefa241a5acf27931e6aae14117c100b036ec",
59
+ "package.json": "778ef2457ab01f302387c811d796b7c61f97feb095725da285655629965f2b5e",
87
60
  "src/base.js": "982f8e46dbb727ec542a494ebdb884ccd19a31ff4e091425b2d1fc01b0d4754a",
88
- "src/boot.js": "911a40e01fa4833249743653b7698e8fed217d52a34056a92524562f3f8daacc",
61
+ "src/boot.js": "62f5cf901d797d53bef584b3c6c0c7fb1a4cc8e2450e9570d6289c72036ed025",
62
+ "src/build/builder.js": "751ea022e1f6f0c6ab79bd4efd6cfe0d705268ff7f699af87dcc64d1871ccc52",
63
+ "src/build/context.js": "4baf1884d366455f3f76f397575d515671585ebf5deb27bc35bc2b1dd9c6bc11",
64
+ "src/build/dockerfile.js": "5957fe3d4ac8ff8b312f980ee04bd1d98ad4373a61f9a33e02f1ff1108b6ea53",
65
+ "src/build/dockerignore.js": "8fc03d09da020f43ea483979b1183891b71d76f012379ab7813838ea0e53c546",
66
+ "src/build/fast.js": "bf7373a43cce995b21751c317a58d18438f9fd2f670266b314b1151ef3cdb9e8",
67
+ "src/build/inspect.js": "52aa26d332c08406e8eeaf8f72661d8acc36bbd4f547a7d68e72b4f0fe265a22",
68
+ "src/build/pin.js": "8c67f017ae3ec560d2638fdfc0bd91ddd758150af16c188bbe3bf2cb4d7e28b2",
69
+ "src/build/tar.js": "25cf477488d9a60da9015be9ca5830bad3ad24428da1546f0621411cb7f44949",
89
70
  "src/calib-worker.js": "3d610591a1b3b58f59c3a2ab457f43c90727d95973cd43055b3b73b76180611d",
90
71
  "src/fs/9p/codec.js": "69711eb4ebc84eff39256138169fe18f5a5066d6cd9e4ff2daf61c65380d7ef7",
91
72
  "src/fs/9p/server.js": "da0ba00ebb5522161bb0ef7cd94bfc9616e342bf865633dfef0ba5bc67627d7a",
@@ -110,7 +91,10 @@
110
91
  "src/host/ckpt-save.js": "e222571033ae74a4439596c5e90e03b2aa9a5516665dd290f8dd243712294523",
111
92
  "src/host/ckpt.js": "2155b5c8e6f609478805eb8a0b5d5210d47cdf35a80f79e0b9ae42b2c3170923",
112
93
  "src/host/clock.js": "aba6af3a8b1a985ae340892222e4b482d33afe20f5c8b3a4c7403ce87d1e247c",
113
- "src/host/ctl.js": "654c55c64efa3df4df8b7b28024a10bd0e7ed3afb89004f8a583b9e62f17f58f",
94
+ "src/host/ctl.js": "ca2ebbb940c9668cb0573c959cd2ff8982f96d58dfdb8dce39d304b89e033dd7",
95
+ "src/host/disk-map.js": "3d7e0d7d488ce25fc6a0e4dc0a9b1c8fc2af96bac1c03491a1d30be392af268b",
96
+ "src/host/disk-worker.js": "9d470cdc72d9e1fbf955e466ec2562abe780f2c6d89174ca3e4fbb1828f8dd76",
97
+ "src/host/disk.js": "373e23539e3916674b7e8708d2e3ae22578214129e56936765db5c8f465cb343",
114
98
  "src/host/errno.js": "26021d85962084be2c43c98fb3accafc2a8f11fbf3c8a5b8d484bd6a088ae4db",
115
99
  "src/host/fs.js": "d6753f74db35b7b01baa8709fc7fdcdfc8d638dfe42f454b0a833b22111e8f4a",
116
100
  "src/host/gcnudge.js": "4a6569cb73ae24a11fc4ac8c467380bd9efba0bb5fa9867b797bc4f153ab15ab",
@@ -127,9 +111,9 @@
127
111
  "src/host/stdio.js": "43973675578eb1d324c2c6d7078e51a6a1f121190ada5ef14a5551364b8c7f5e",
128
112
  "src/host/threads.js": "952381940e7f43114867e9a9714528f2f20c8064b6b0f2018ab075128dc601be",
129
113
  "src/host/wasi.js": "fbafa239b8fb4083f7e8c910b1adab38901e8e7cffd247284a9abb2847eaf4bd",
130
- "src/host/wmd-imports.js": "094f994758c239b75177781c0613decbbd926090b0e8974163ce7caf8a99234d",
114
+ "src/host/wmd-imports.js": "3a00562f7c9c7787ab3841b14dde5d903a78f43ed19c43a2f5e31fd8a14c09d2",
131
115
  "src/host/yamux.js": "f4044f14f7e836224bdb4b1e53e59c565ee478b83b2e428fffb9f7a048768f79",
132
- "src/image-files.js": "539856f3991b9f89202dd04ec3816ba8bce7787034ab810534c5170a205ffde7",
116
+ "src/image-files.js": "7bac16bd278d17ba291dfe4771564627b5efbdda927b429018a75e8b906dd4cd",
133
117
  "src/net/dhcp-dns.js": "fdfa7c7d5cbbb4a8774b7dee195bf2df22a0c3fe8f20ac9694b24485fa8f25d5",
134
118
  "src/net/net-worker.js": "0a2ce998f69807e386ded7ff1bee53687a1d59ed76d7ef46e2eb6f958b14ad38",
135
119
  "src/net/packet.js": "d658f26fa84324a88d7dbc26af9c4a9c10e977c3060f16f7db431496eacd1127",
@@ -143,12 +127,13 @@
143
127
  "src/net/wisp.js": "06c50159f416d40a912fe2cce4806388b938f2ab0220b2ee6da890630ce3d113",
144
128
  "src/registry/client.js": "fd38550e9afd82c5cb498513d245c33357a71697b3b2e6dd78538934bcc9d879",
145
129
  "src/registry/converter.js": "1fb8294fd5275f6772c27130cc9ba70bd46fafaf440884d42448036618229f37",
146
- "src/registry/hub.js": "22323e0f799a7078843514c9f87bbcdaa4173fd991ad4e059377befa773762d4",
130
+ "src/registry/hub.js": "cd672323bd1f96bd1d7e9eed80d4f36891e919e456fc0deb378fa0c7e3b6f249",
131
+ "src/registry/load-tar.js": "9dd2b5d8884c64247339b141ab5a57b8ab1a2e007c32aa45b1103b37405b0ed7",
147
132
  "src/registry/oci.js": "6a51ea16b34a65c0e732730f3e07d7ad16c50f6943a486ec05ca6aa3e98676a1",
148
133
  "src/registry/pack-worker.js": "fd6798851cb7815dacfcb67fc78c41654209c5504d8fd343aae524e99de1c741",
149
- "src/registry/pull.js": "66edf4602aca8d41cc68393d01a2da01c4667de7c493af5ae400e8f249d4e3ae",
134
+ "src/registry/pull.js": "24f2e01a76c9f7f27896af69b989f8c97f0ca31712b3e8110aaa5b5e3148b04b",
150
135
  "src/registry/ref.js": "24038e0fbcb6ac143cb8e8f7b9fac4dd0803d0891961786dc88dba01a4402e7b",
151
136
  "src/registry/wasi-mem.js": "a7f684343010892460db27ee8c1e38e47e797c93dc4ebe4c30185dca6ca56ee2",
152
- "src/worker.js": "fe23f1b99acedc65e56f92f7246923fdbdbefd085bae427b57cbe1402b4f5161"
137
+ "src/worker.js": "c654be359e741d5b1fde1be05f8909968d4e00d1fca61be169e60ea691daa662"
153
138
  }
154
139
  }
package/guest/arm64/Image CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/guest/arm64/Image
3
- size: 39047176 bytes
4
- sha256: a6b3209cf69d5bc1dacf0f3ed67f92c535dbb4108d67b47091c2aef87c6d7b04
3
+ size: 39049224 bytes
4
+ sha256: 1d296c86b1fee0a10d0c2571b7e19854fe85ca0250cc9fadfe75eb268e48be7b
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/guest/arm64/Image-4k
3
3
  size: 38828040 bytes
4
- sha256: 8ff6e338a0b42d9f774b35423e0e7f9d7f779c635446e8de2a3beb83d3518f8a
4
+ sha256: 6296e3d9fc7bb06a26340bb4453b5f0970d6b55452903546ba2c762820e2df5b
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadedstack/wmd-web",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "license": "MIT",
5
5
  "homepage": "https://github.com/threadedstack/wmd-releases",
6
6
  "repository": {
@@ -9,9 +9,9 @@
9
9
  },
10
10
  "description": "Browser host for wmd's guest vm.wasm (Web Workers + shared memory + WASI preview1)",
11
11
  "wmd": {
12
- "tag": "v0.1.17",
13
- "commit": "fa2b21cfd0fd0bf679498e6feeefc26c33f17372",
14
- "web": "0.3.2"
12
+ "tag": "v0.1.19",
13
+ "commit": "89688f12e0caef39ab0a159b39c00f3525e31051",
14
+ "web": "0.4.0"
15
15
  },
16
16
  "type": "module",
17
17
  "main": "./src/boot.js",
package/relay/hosted.ts CHANGED
@@ -18,6 +18,7 @@ import { Bucket, type Session, type Stream } from './engine.ts'
18
18
  import { type Addr, type Cidr, addrString, cidrContains, hostedBlockedReason, parseCidr, parseIP } from './policy.ts'
19
19
 
20
20
  const HOUR = 3_600_000
21
+ const MINUTE = 60_000
21
22
 
22
23
  export type HostedOptions = {
23
24
  abuseContact: string
@@ -26,11 +27,12 @@ export type HostedOptions = {
26
27
  now?: () => number // ms since the epoch (tests)
27
28
  warn?: (line: string) => void // one-line operator notices (stderr)
28
29
  sites?: string[] // A: page origin patterns served (required: none listed serves no page)
29
- sitesPerAddrPerHour?: number // B: different sites per address group per hour (200: plan §16)
30
+ sitesPerAddrPerHour?: number // B: different sites per address group per hour (800)
30
31
  sitesPerNetPerHour?: number // B: per network (800)
31
32
  monthBytes?: number // C: bytes relayed per UTC month (the operator's figure from the provider's free outbound data); 0 = no cap
32
33
  usageFile?: string // C: the month's counter survives a restart here
33
34
  dialLimitMs?: number // D, G: the engine's dial limit (connectTimeoutMs, 10 s)
35
+ failedDialsPerMin?: number // D: failed connections per address group within a minute before any is slowed (20)
34
36
  fdLimit?: number // E: the open-file limit (read from the host when absent)
35
37
  fdOpen?: number // E: descriptors open at start (read from the host when absent)
36
38
  dnsServers?: number // E: resolvers the relay asks (each may hold a UDP and a TCP socket)
@@ -256,10 +258,11 @@ export function createGovernor(o: HostedOptions) {
256
258
  const warn = o.warn ?? ((m: string) => void process.stderr.write(`${m}\n`))
257
259
  const host = o.fdLimit === undefined || o.fdOpen === undefined ? hostLimits() : null
258
260
  const c = {
259
- sitesPerAddrPerHour: o.sitesPerAddrPerHour ?? 200,
261
+ sitesPerAddrPerHour: o.sitesPerAddrPerHour ?? 800,
260
262
  sitesPerNetPerHour: o.sitesPerNetPerHour ?? 800,
261
263
  monthBytes: o.monthBytes ?? 0,
262
264
  dialLimitMs: o.dialLimitMs ?? 10_000,
265
+ failedDialsPerMin: o.failedDialsPerMin ?? 20,
263
266
  fdLimit: o.fdLimit ?? host!.fdLimit,
264
267
  fdOpen: o.fdOpen ?? host!.fdOpen,
265
268
  dnsServers: o.dnsServers ?? getServers().length,
@@ -427,12 +430,16 @@ export function createGovernor(o: HostedOptions) {
427
430
  }
428
431
 
429
432
  // ---- D
430
- const dials = new Map<string, { w: number; gate: number; told: boolean }>()
433
+ const dials = new Map<string, { w: number; gate: number; told: boolean; fails: number[] }>()
431
434
  const dialOf = (grp: string) => {
432
435
  let d = dials.get(grp)
433
- if (!d) dials.set(grp, (d = { w: 0, gate: 0, told: false }))
436
+ if (!d) dials.set(grp, (d = { w: 0, gate: 0, told: false, fails: [] }))
434
437
  return d
435
438
  }
439
+ const recentFails = (d: { fails: number[] }, t: number) => {
440
+ while (d.fails.length && t - d.fails[0] >= MINUTE) d.fails.shift()
441
+ return d.fails.length
442
+ }
436
443
  function dialGate(s: Session): Why | { waitMs: number } | null {
437
444
  const d = dials.get(grpOf(s))
438
445
  if (!d || !d.w) return null
@@ -504,15 +511,22 @@ export function createGovernor(o: HostedOptions) {
504
511
  /** D: a dial was refused, found no route, timed out or named a host that does not exist, after `ms`. */
505
512
  function dialFailed(s: Session, ms: number) {
506
513
  const d = dialOf(grpOf(s))
514
+ const t = now()
515
+ d.fails.push(t)
516
+ if (d.fails.length > c.failedDialsPerMin) d.fails.shift()
517
+ if (!d.w && recentFails(d, t) < c.failedDialsPerMin) return
507
518
  d.w = Math.min(c.dialLimitMs, Math.max(1, Math.round(ms), d.w * 2))
508
519
  }
509
520
  function dialOk(s: Session) {
510
521
  const d = dials.get(grpOf(s))
511
522
  if (!d) return
523
+ if (!d.w) return
512
524
  d.w = Math.floor(d.w / 2)
513
525
  if (!d.w) {
514
- dials.delete(grpOf(s))
526
+ d.gate = 0
515
527
  if (d.told) s.hx?.handle.tell?.(3, 'failed-dials', 0, '')
528
+ d.told = false
529
+ if (!recentFails(d, now())) dials.delete(grpOf(s))
516
530
  }
517
531
  }
518
532
  const dialWait = (s: Session) => dials.get(grpOf(s))?.w ?? 0
@@ -616,12 +630,13 @@ export function createGovernor(o: HostedOptions) {
616
630
  ticker.unref?.()
617
631
  const saver = o.usageFile ? setInterval(saveUsage, 15_000) : null
618
632
  saver?.unref?.()
619
- const sweep = setInterval(() => {
633
+ function sweep() {
620
634
  const t = now()
621
635
  for (const m of [addrSites, netSites]) for (const [k, x] of m) if (!fresh(x, t).size) m.delete(k)
622
- for (const [k, d] of dials) if (d.gate + HOUR < t) dials.delete(k)
623
- }, 60_000)
624
- sweep.unref?.()
636
+ for (const [k, d] of dials) if (d.gate + HOUR < t && !recentFails(d, t)) dials.delete(k)
637
+ }
638
+ const sweeper = setInterval(sweep, 60_000)
639
+ sweeper.unref?.()
625
640
 
626
641
  const state = () => {
627
642
  roll()
@@ -684,6 +699,7 @@ export function createGovernor(o: HostedOptions) {
684
699
  bytes,
685
700
  limitWhy,
686
701
  tick,
702
+ sweep,
687
703
  slowed,
688
704
  take,
689
705
  waitMs,
@@ -691,11 +707,11 @@ export function createGovernor(o: HostedOptions) {
691
707
  blockedAddr,
692
708
  liveStreams: () => used,
693
709
  limitsLine: () =>
694
- `only listed sites; ${c.sitesPerAddrPerHour} different sites an hour per address, ${c.sitesPerNetPerHour} per network; failed dials slow the sender` +
710
+ `only listed pages may use this relay; ${c.sitesPerAddrPerHour} different sites an hour per address, ${c.sitesPerNetPerHour} per network; failed dials slow the sender` +
695
711
  (c.monthBytes ? `; ${Math.round(c.monthBytes / 1e9)} GB a month, heavy users slowed first when the month runs ahead` : ''),
696
712
  stop() {
697
713
  clearInterval(ticker)
698
- clearInterval(sweep)
714
+ clearInterval(sweeper)
699
715
  if (saver) clearInterval(saver)
700
716
  saveUsage()
701
717
  },
package/relay/main.ts CHANGED
@@ -53,7 +53,7 @@ Flags:
53
53
  --allow-loopback-ports <list> guests may reach this computer's loopback on these ports
54
54
  --block-ports <list> destination ports refused (none)
55
55
  --allow-hosts <list> only these names (name[:port|port], a leading . for subdomains)
56
- --hosted the public hosted profile (docs/relay-hosted.md): only listed sites, no crawling,
56
+ --hosted the public hosted profile (docs/relay-hosted.md): only listed pages may use this relay, no crawling,
57
57
  failed dials slowed, the month's data cap, host limits, deny lists, a control
58
58
  socket, /healthz, /readyz
59
59
  --abuse-contact <text> required: an address or URL, shown in the MOTD, /healthz and every refusal
@@ -79,7 +79,7 @@ const FLAG_KEYS = ['host', 'port', 'origins', 'keys', 'tlsCert', 'tlsKey', 'loca
79
79
  const CAP_KEYS = ['maxConns', 'maxSessions', 'maxSessionsPerGroup', 'opensPerMin', 'connectsPerMin', 'dnsPerMin', 'rateDown', 'rateUp', 'maxBytes', 'selfAddrs', 'dnsServers', 'connectTimeoutMs', 'dnsTimeoutMs'] as const
80
80
  const WISP_KEYS = ['path', 'buffer', 'maxPayload', 'maxHeldBytes', 'maxSessionHeldBytes', 'maxHeldTotal', 'maxGroupHeldBytes', 'instancesPerHour', 'maxPrincipals', 'preAuthPerAddr', 'handshakeMs', 'streamIdleMs', 'prefixKeys'] as const
81
81
  // the numbers of the hosted rules, under "hosted" in a config file (hosted.ts HostedOptions; all whole numbers)
82
- const HOSTED_KEYS = ['sitesPerAddrPerHour', 'sitesPerNetPerHour'] as const
82
+ const HOSTED_KEYS = ['sitesPerAddrPerHour', 'sitesPerNetPerHour', 'failedDialsPerMin'] as const
83
83
  // keys of the limits plan §16 removed: a config that still has them starts, with one line saying they do nothing
84
84
  const LEGACY_KEYS = new Set(['maxBytesPerDay', 'maxBytesPerMonth'])
85
85
  const LEGACY_HOSTED_KEYS = new Set(['groupStreams', 'groupConnectsPerMin', 'groupBytesPerHour', 'groupDnsPerMin', 'coarseSessions', 'coarseInstancesPerHour', 'coarseRateDown', 'coarseRateUp', 'originTable', 'originSessions', 'originShare', 'otherOriginSessions', 'noOriginSessions', 'originsPerAddrPerHour', 'originRateDown', 'originRateUp', 'maxStreams', 'maxRateDown', 'maxRateUp', 'destConnectsPerMin', 'destStreams', 'distinctDests', 'failedConnects', 'failedWindowMs', 'failedCoolMs'])
@@ -326,7 +326,7 @@ export function relayOptions(c: Conf, env: Record<string, string | undefined> =
326
326
  if (c.behindTls && !isPrivateBind(host)) throw new UsageError(`--hosted --behind-tls binds only to loopback or a private address (the proxy\'s side), not ${host}: a public bind would let anyone skip the proxy and its TLS`)
327
327
  if (!tls && !c.behindTls && !isPrivateBind(host)) throw new UsageError(`--hosted refuses plain ws:// on the public address ${host}: give --tls-cert and --tls-key, or put a TLS proxy in front with --behind-tls (bound to loopback)`)
328
328
  if (c.localTls) throw new UsageError('--local-tls is for a person\'s own computer, not --hosted')
329
- if (!c.sitesFile) throw new UsageError('--hosted needs --sites-file <file>: the page origins it serves (JSON {"sites": [...]}); a hosted relay serves only listed sites')
329
+ if (!c.sitesFile) throw new UsageError('--hosted needs --sites-file <file>: the page origins it serves (JSON {"sites": [...]}); only listed pages may use this relay')
330
330
  }
331
331
  const w = c.wisp ?? {}
332
332
  const secret = env.WMD_RELAY_SECRET || randomBytes(32).toString('base64url') // tokens (wmn's issuer) only when the secret is shared
package/src/boot.js CHANGED
@@ -51,9 +51,11 @@ import {
51
51
  } from './host/ckpt.js'
52
52
  import { captureJit, jitSnapStats, modulesOf, ownersNeedingHub, restoreStore } from './host/jit-snap.js'
53
53
  export { CheckpointError, memoryStore, opfsStore } from './host/ckpt.js'
54
+ export { DiskError, deleteDisk, openDisk, sweepDisks } from './host/disk.js'
54
55
  export { NetSwitch } from './net/switch.js'
55
56
  export { ImageFiles, openImage, parseGroup, parsePasswd, resolveUser } from './image-files.js'
56
57
  export { P9_CACHE_DEFAULT, p9InitCmdline, p9MountOptions } from './host/p9mount.js'
58
+ export { loadImageTar } from './registry/load-tar.js'
57
59
 
58
60
  const PAGE = 65536
59
61
  /** A trap with the memory this close to its maximum is reported as out of memory. */
@@ -414,12 +416,30 @@ function toShared(bytes) {
414
416
  return sab
415
417
  }
416
418
 
419
+ /** vm.wasm's arguments for a machine with `disk`: --swap <MiB> and wmd.disk=data appended to --append. */
420
+ export function diskArgs(args, disk) {
421
+ if (args.includes('--swap')) throw new TypeError('--swap and a disk: give one')
422
+ const mib = disk.bytes / 2 ** 20
423
+ if (!Number.isInteger(mib) || mib < 1) throw new TypeError(`disk size ${disk.bytes} is not whole MiB`)
424
+ const out = [...args, '--swap', String(mib)]
425
+ const i = out.indexOf('--append')
426
+ if (i < 0 || i + 1 >= out.length) throw new TypeError('a disk needs --append (the kernel line gets wmd.disk=data)')
427
+ // the disk holds data: a wmd.disk= of any other value would let the guest init write a swap header over it
428
+ const given = out[i + 1].match(/(?:^| )wmd\.disk(?:=(\S*))?(?= |$)/g)
429
+ if (given && given.some(g => g.trim() !== 'wmd.disk=data')) throw new TypeError(`a disk needs wmd.disk=data on the kernel line, not ${given.map(g => g.trim()).join(' ')}`)
430
+ if (!given) out[i + 1] = `${out[i + 1]} wmd.disk=data`
431
+ return out
432
+ }
433
+
417
434
  /**
418
435
  * @param {object} o
419
436
  * @param {string|Uint8Array|ArrayBuffer} o.vmUrl URL of vm.wasm, or its bytes
420
437
  * @param {Record<string,string|Uint8Array>} o.files preopen contents: path → URL or bytes
421
438
  * @param {string[]} o.args vm.wasm arguments (after argv[0])
422
439
  * @param {string[]} [o.env] "K=V" (the runner passes only WMD_*)
440
+ * @param {object} [o.disk] a disk from openDisk (host/disk.js): the guest gets it as virtio-blk
441
+ * /dev/vda (vm.wasm's --swap MiB sizes the device) with wmd.disk=data on the kernel line, so guest init leaves
442
+ * it alone. A machine with a disk takes no checkpoints: guest memory and the disk would not match
423
443
  * @param {boolean} [o.heapReserve] W21 (default true, false in mobile WebKit: heapReserveDefault, D-516): vm.wasm grows its memory to the maximum
424
444
  * before any thread starts (WMD_HEAP_RESERVE=1), so memory never grows under running
425
445
  * vCPUs. Chromium checks memory.fill/copy against a per-Worker size that lags another
@@ -520,6 +540,10 @@ export async function bootWmd(o) {
520
540
  o.onStderr?.(text)
521
541
  }
522
542
  const preopen = o.preopen || '/g'
543
+ if (o.disk) {
544
+ if (o.checkpoint || o.restore || o.restoreLazy) throw new TypeError('a machine with a disk takes no checkpoints')
545
+ o = { ...o, args: diskArgs(o.args, o.disk) }
546
+ }
523
547
  const argv = ['vm.wasm', ...o.args]
524
548
  const env = vmEnv(o.env, o.heapReserve ?? heapReserveDefault())
525
549
  // the default Worker is started from a literal new URL(..., import.meta.url) so bundlers (Vite, webpack)
@@ -1550,6 +1574,7 @@ export async function bootWmd(o) {
1550
1574
  tableMin,
1551
1575
  memory,
1552
1576
  ram: ramMem,
1577
+ blk: o.disk?.mailbox || null,
1553
1578
  ctlSab,
1554
1579
  files,
1555
1580
  preopen,