@threadedstack/wmd-web 0.1.18 → 0.1.20

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
@@ -159,10 +159,11 @@ The interface this host implements is in [docs/host-interface.md](docs/host-inte
159
159
 
160
160
  `createBuilder` runs BuildKit (moby/buildkit v0.33.0) in a privileged builder machine in the page and builds a
161
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).
162
+ Chromium, Firefox and WebKit all pass the same 18 build steps, and Node and Go projects for arm64 and amd64 (plan
163
+ `docs/plans/web-buildkit-impl.md`, section 10).
163
164
 
164
165
  ```js
165
- import { createBuilder } from 'wmd-web/build'
166
+ import { buildImage, createBuilder } from 'wmd-web/build'
166
167
 
167
168
  const builder = await createBuilder({
168
169
  vmUrl, files, // as bootWmd: the engine and the builder arch's guest files
@@ -176,6 +177,9 @@ const builder = await createBuilder({
176
177
  const r = await builder.build({ context: { Dockerfile: 'FROM alpine:3.20\nRUN echo hi > /x\n' } })
177
178
  // r: {index, packs, log, path: 'fast'|'buildkit', ms, cached}
178
179
  await builder.close()
180
+
181
+ // or, for one build: createBuilder + build + close in one call
182
+ const r2 = await buildImage({ ...builderOptions, context, timeoutMs: 30 * 60_000 })
179
183
  ```
180
184
 
181
185
  - **The cache stays.** The disk outlives the builder and the visit. After a reload, or after a tab was killed
@@ -190,7 +194,24 @@ await builder.close()
190
194
  for layer the same as BuildKit's (40 compared cases): under 0.4 s once its base image is pulled, where a warm
191
195
  BuildKit takes 5 to 14 s.
192
196
  - **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.
197
+ or `'secret'`. Cancelling with `signal` stops the step and leaves nothing behind. A failed build also carries `jit`
198
+ (the engine's last JIT messages), `net` (the guest network's counters and events), and, when the machine itself
199
+ stopped, `machine` (why, and its last output).
200
+ - **RAM and vCPUs.** The default is 1536 MiB and 2 vCPUs (`ram`, `cpus`). Small builds and the proxy project in the
201
+ plan fit. A build that compiles a native addon or bundles a large app needed 3072 MiB and 4 vCPUs (plan 10.5); a
202
+ single `RUN` step must finish within `timeoutMs`, since BuildKit resumes from its cache only between steps.
203
+ - **WebKit** builds Go and Node images and the real projects of the plan, but slower: its code space fills and new
204
+ code runs in the interpreter for a while (a `{t:'slow', reason:'webkit-code-space'}` event says so).
205
+ - In WebKit, node in `RUN` steps runs its JavaScript in the interpreter (D-553, `nodeInterpreted`, an
206
+ `{t:'node-interpreted'}` event): node's own JIT writes code faster than WebKit's code pool takes it, and a
207
+ 1,600-package `pnpm` install did not end in 27 minutes. A preload bound at `/dev/.wmd-node.js` and named in
208
+ `NODE_OPTIONS` (`--require`) turns off V8's compilers; WebAssembly still works. The image and the cache keys do
209
+ not change. `nodeInterpreted: false` turns it off; `true` turns it on elsewhere.
210
+ - npm closes a TLS handshake that is not done 15 s after the connect. The guest network lets 4 handshakes go at
211
+ once, sends a ClientHello that waited on a fresh connection, and sends an unanswered one again once (D-552); it
212
+ dials a stream again when the remote closes it before any byte crossed (D-551).
213
+ - WebKit cannot read a Blob of 128 MiB or more whole (`NotReadableError`). Read the `tar` Blob a build returns
214
+ with `blobBytes(blob)` from `wmd-web/build`, which reads it in slices (D-554).
194
215
  - **The builder images** are built with `make web-builder` (wmd builds them; Docker Desktop is not used) into
195
216
  `web/dist/builder/builder-<arch>.tar`, and their sha256 is pinned in `src/build/pin.js`. `make web-release` puts
196
217
  them next to the release tgz.
package/RELEASE.json CHANGED
@@ -1,66 +1,50 @@
1
1
  {
2
2
  "name": "wmd-web",
3
- "version": "0.4.0",
4
- "commit": "cb2d3874a3d65236337c76c3c59147fe152a010c",
5
- "previous": "wmd-web-v0.1.0",
3
+ "version": "0.5.1",
4
+ "commit": "f0089e9f12e73d951b5a3a47f962764edc1c39a3",
5
+ "previous": "wmd-web-v0.5.0",
6
6
  "decisions": [
7
- "D-486",
8
- "D-488",
9
- "D-494",
10
- "D-498",
11
- "D-505",
12
- "D-506",
13
- "D-507",
14
- "D-509",
15
- "D-511",
16
- "D-516",
17
- "D-518",
18
- "D-531",
19
- "D-533",
20
- "D-539",
21
- "D-542",
22
- "D-544",
23
- "D-545",
24
- "D-547",
25
- "D-548"
7
+ "D-552",
8
+ "D-554"
26
9
  ],
27
- "vmSources": "7a707b82c43d3f1eb999476cad68e7ce79cc9cb5",
10
+ "vmSources": "543f2ee1a363719c2a973fff350d54a54d325286",
28
11
  "guest": {
29
12
  "arch": "arm64",
30
13
  "files": {
31
14
  "vm.wasm": {
32
- "sha256": "40299e37e788189e6d3bea9d9cdaefa241a5acf27931e6aae14117c100b036ec",
33
- "bytes": 1561499
15
+ "sha256": "73bee71d7453a130f7991c7cead3bc12787050116217f635f9f523f584d2577d",
16
+ "bytes": 1562868
34
17
  },
35
18
  "Image": {
36
- "sha256": "1d296c86b1fee0a10d0c2571b7e19854fe85ca0250cc9fadfe75eb268e48be7b",
19
+ "sha256": "6952a8d4f1bafec9c3e3b17e2c4af20ab5de8a7086956d087979804be2a01353",
37
20
  "bytes": 39049224
38
21
  },
39
22
  "Image-4k": {
40
- "sha256": "6296e3d9fc7bb06a26340bb4453b5f0970d6b55452903546ba2c762820e2df5b",
23
+ "sha256": "47a1dcf06bcd1cc31fb4f02adbd1e035bc9e9a243fe57f3efd71b4a287581de2",
41
24
  "bytes": 38828040
42
25
  },
43
26
  "initramfs.cpio.gz": {
44
- "sha256": "f008aa9e0f6b4e913242f9982d3b194dba7dc39042a24bc89be64630672125af",
45
- "bytes": 4685204
27
+ "sha256": "efa6eb2338cb01d87eeebce8035ed7a26b00edcfc3b9db963b38272715d2ee9d",
28
+ "bytes": 4683846
46
29
  }
47
30
  }
48
31
  },
49
32
  "files": {
50
- "README.md": "3c12ca29aab5160b955ba3f50035f19be1dbf0278c4fb006e253a37ac9efbe6a",
33
+ "README.md": "6962e410444bcf44ba6dd8498673e2de17ee4f8b96e1d29044a55b8f0dbcd8de",
51
34
  "export-page/coi-sw.js": "0300e670b413cb2e0aae35843fc583546bbf6687df971ab6df41cfa3b0d494d7",
52
35
  "export-page/index.html": "f5450c89e63601bbbbffef421f03491b231d1f143070012c1c5315d4b0059f43",
53
36
  "export-page/run.css": "b57adf1e76553549c1f89ff157b7973409dedc21889cd3932948cd05b51adc6f",
54
37
  "export-page/run.js": "717636a632a40469106ddf1383e198eaac5d83651b5866d0699ec04b82462b7f",
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",
38
+ "guest/arm64/Image": "6952a8d4f1bafec9c3e3b17e2c4af20ab5de8a7086956d087979804be2a01353",
39
+ "guest/arm64/Image-4k": "47a1dcf06bcd1cc31fb4f02adbd1e035bc9e9a243fe57f3efd71b4a287581de2",
40
+ "guest/arm64/initramfs.cpio.gz": "efa6eb2338cb01d87eeebce8035ed7a26b00edcfc3b9db963b38272715d2ee9d",
41
+ "guest/arm64/vm.wasm": "73bee71d7453a130f7991c7cead3bc12787050116217f635f9f523f584d2577d",
42
+ "package.json": "7d43c05047d0a3ea56b4a7709f48a60de32e15c3bdf328d55435c46637981b40",
60
43
  "src/base.js": "982f8e46dbb727ec542a494ebdb884ccd19a31ff4e091425b2d1fc01b0d4754a",
61
- "src/boot.js": "62f5cf901d797d53bef584b3c6c0c7fb1a4cc8e2450e9570d6289c72036ed025",
62
- "src/build/builder.js": "751ea022e1f6f0c6ab79bd4efd6cfe0d705268ff7f699af87dcc64d1871ccc52",
63
- "src/build/context.js": "4baf1884d366455f3f76f397575d515671585ebf5deb27bc35bc2b1dd9c6bc11",
44
+ "src/blob.js": "bb2a2f0d87ce236280a7be2f246c926010dfe07f649c27da5e70d8eb5eaf7061",
45
+ "src/boot.js": "b1819d2b8abd46fd42f10a986719a311bedca705fc368a4cd19cc121b04b57d2",
46
+ "src/build/builder.js": "0d5355c8570b0ecedce25602ff7f60c49b7daaed669426328f05f52e26048213",
47
+ "src/build/context.js": "d38097c61dbdadc4d41290c010425a4422f499aba473fa2b83f2a5c41e543571",
64
48
  "src/build/dockerfile.js": "5957fe3d4ac8ff8b312f980ee04bd1d98ad4373a61f9a33e02f1ff1108b6ea53",
65
49
  "src/build/dockerignore.js": "8fc03d09da020f43ea483979b1183891b71d76f012379ab7813838ea0e53c546",
66
50
  "src/build/fast.js": "bf7373a43cce995b21751c317a58d18438f9fd2f670266b314b1151ef3cdb9e8",
@@ -93,8 +77,8 @@
93
77
  "src/host/clock.js": "aba6af3a8b1a985ae340892222e4b482d33afe20f5c8b3a4c7403ce87d1e247c",
94
78
  "src/host/ctl.js": "ca2ebbb940c9668cb0573c959cd2ff8982f96d58dfdb8dce39d304b89e033dd7",
95
79
  "src/host/disk-map.js": "3d7e0d7d488ce25fc6a0e4dc0a9b1c8fc2af96bac1c03491a1d30be392af268b",
96
- "src/host/disk-worker.js": "9d470cdc72d9e1fbf955e466ec2562abe780f2c6d89174ca3e4fbb1828f8dd76",
97
- "src/host/disk.js": "373e23539e3916674b7e8708d2e3ae22578214129e56936765db5c8f465cb343",
80
+ "src/host/disk-worker.js": "6e82bfeb4aa3f8b02bd7cb2de7427def26114e016f6b05196c8485d7fc8315ee",
81
+ "src/host/disk.js": "e941e62cc2d9a83582d53c80c09c5f0f682508f77390237bedff4051c489b2cd",
98
82
  "src/host/errno.js": "26021d85962084be2c43c98fb3accafc2a8f11fbf3c8a5b8d484bd6a088ae4db",
99
83
  "src/host/fs.js": "d6753f74db35b7b01baa8709fc7fdcdfc8d638dfe42f454b0a833b22111e8f4a",
100
84
  "src/host/gcnudge.js": "4a6569cb73ae24a11fc4ac8c467380bd9efba0bb5fa9867b797bc4f153ab15ab",
@@ -103,7 +87,7 @@
103
87
  "src/host/jit-hub-core.js": "f8d39dd0e4cbd42568bf56a8a7105f321d33bcf2d5faf8756d45a1ed692bd344",
104
88
  "src/host/jit-hub.js": "cf3fc9f9d4e3fb50c41d6de5e2523f380d3c6c1feaf61c15d90cde89aec2f4c3",
105
89
  "src/host/jit-snap.js": "3932f36047c9edeb7f59282d1c37fef19c8f5a9fe4bd0192fb93aa64030fe51d",
106
- "src/host/jit.js": "cb39f90df63752a7d4077f6972731cf4b1d8026cb4ca594e32f9203be6fe1edb",
90
+ "src/host/jit.js": "72ef7334506fde2b3724cee93b52a987fbd6d45473b8de93d914db8a80ae9e17",
107
91
  "src/host/layout.js": "d5bc5aa9292630b204438df5d6278fda6daa2a65acc061e1c57e0272b32fecc8",
108
92
  "src/host/monitor.js": "ae1741ba35668bcc6192642246494433b175668933921634c699af5a0a69acc0",
109
93
  "src/host/p9mount.js": "43246601fc1e27d5294742e4482ed166a50c665cb5f526c3646a47dd27e06150",
@@ -113,14 +97,14 @@
113
97
  "src/host/wasi.js": "fbafa239b8fb4083f7e8c910b1adab38901e8e7cffd247284a9abb2847eaf4bd",
114
98
  "src/host/wmd-imports.js": "3a00562f7c9c7787ab3841b14dde5d903a78f43ed19c43a2f5e31fd8a14c09d2",
115
99
  "src/host/yamux.js": "f4044f14f7e836224bdb4b1e53e59c565ee478b83b2e428fffb9f7a048768f79",
116
- "src/image-files.js": "7bac16bd278d17ba291dfe4771564627b5efbdda927b429018a75e8b906dd4cd",
100
+ "src/image-files.js": "19f90a7d3c8accae5093e53bb85fe7e158ac1dd18866283722171eccb1d7e37a",
117
101
  "src/net/dhcp-dns.js": "fdfa7c7d5cbbb4a8774b7dee195bf2df22a0c3fe8f20ac9694b24485fa8f25d5",
118
102
  "src/net/net-worker.js": "0a2ce998f69807e386ded7ff1bee53687a1d59ed76d7ef46e2eb6f958b14ad38",
119
103
  "src/net/packet.js": "d658f26fa84324a88d7dbc26af9c4a9c10e977c3060f16f7db431496eacd1127",
120
104
  "src/net/peer.js": "d83cf85639f6197fe26c877eb79f8c9afc017bf2362dc2b22a8b6bd0f45f3f19",
121
105
  "src/net/proto.js": "abfc8cd6f4331cbc98fcb1b0bba0795ff3042cc7db10476673ead1723249eee0",
122
106
  "src/net/relay-client.js": "4101de76dc0251db46839dc786cce677e4020795adf022bdce14bcf7edf8f864",
123
- "src/net/stack.js": "cd451cce7130f08e59cc19c961d2a5fca8416e577a40df1419ab6d5046018891",
107
+ "src/net/stack.js": "b721e4e116b0611abfe8ec1c6f2e212efd89a11f745fef2222ca5de474d8c0a9",
124
108
  "src/net/switch.js": "bb047fb1ceeb10a84b5475916ead916362accf0e6f42a86561653dfad0c6c50d",
125
109
  "src/net/wisp-client.js": "653961c4b735f2f74e5ea89e75c387f15ec8b8d9ed4c45d06c7d6b2a04968d42",
126
110
  "src/net/wisp-transport.js": "2b71201f3c686078c867e8995bf5c1cd6d47a0d415f476f3a33fbd5729a55f74",
@@ -128,7 +112,7 @@
128
112
  "src/registry/client.js": "fd38550e9afd82c5cb498513d245c33357a71697b3b2e6dd78538934bcc9d879",
129
113
  "src/registry/converter.js": "1fb8294fd5275f6772c27130cc9ba70bd46fafaf440884d42448036618229f37",
130
114
  "src/registry/hub.js": "cd672323bd1f96bd1d7e9eed80d4f36891e919e456fc0deb378fa0c7e3b6f249",
131
- "src/registry/load-tar.js": "9dd2b5d8884c64247339b141ab5a57b8ab1a2e007c32aa45b1103b37405b0ed7",
115
+ "src/registry/load-tar.js": "219d6701f5c938e27e3ca36ade8fcea8114a5f2b1511496f0dbd60137290867f",
132
116
  "src/registry/oci.js": "6a51ea16b34a65c0e732730f3e07d7ad16c50f6943a486ec05ca6aa3e98676a1",
133
117
  "src/registry/pack-worker.js": "fd6798851cb7815dacfcb67fc78c41654209c5504d8fd343aae524e99de1c741",
134
118
  "src/registry/pull.js": "24f2e01a76c9f7f27896af69b989f8c97f0ca31712b3e8110aaa5b5e3148b04b",
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
3
  size: 39049224 bytes
4
- sha256: 1d296c86b1fee0a10d0c2571b7e19854fe85ca0250cc9fadfe75eb268e48be7b
4
+ sha256: 6952a8d4f1bafec9c3e3b17e2c4af20ab5de8a7086956d087979804be2a01353
@@ -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: 6296e3d9fc7bb06a26340bb4453b5f0970d6b55452903546ba2c762820e2df5b
4
+ sha256: 47a1dcf06bcd1cc31fb4f02adbd1e035bc9e9a243fe57f3efd71b4a287581de2
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadedstack/wmd-web",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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.18",
13
- "commit": "cb2d3874a3d65236337c76c3c59147fe152a010c",
14
- "web": "0.4.0"
12
+ "tag": "v0.1.20",
13
+ "commit": "f0089e9f12e73d951b5a3a47f962764edc1c39a3",
14
+ "web": "0.5.1"
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); a publish identity three times that
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)
@@ -57,7 +59,6 @@ export type Handle = { ip: string; grp: string; origin: string; end: (why: Why)
57
59
  /** What a session holds from the governor until it ends. */
58
60
  export type Admission = {
59
61
  grp: string
60
- publish: boolean
61
62
  net: string
62
63
  handle: Handle
63
64
  fair: Bucket | null
@@ -257,10 +258,11 @@ export function createGovernor(o: HostedOptions) {
257
258
  const warn = o.warn ?? ((m: string) => void process.stderr.write(`${m}\n`))
258
259
  const host = o.fdLimit === undefined || o.fdOpen === undefined ? hostLimits() : null
259
260
  const c = {
260
- sitesPerAddrPerHour: o.sitesPerAddrPerHour ?? 200,
261
+ sitesPerAddrPerHour: o.sitesPerAddrPerHour ?? 800,
261
262
  sitesPerNetPerHour: o.sitesPerNetPerHour ?? 800,
262
263
  monthBytes: o.monthBytes ?? 0,
263
264
  dialLimitMs: o.dialLimitMs ?? 10_000,
265
+ failedDialsPerMin: o.failedDialsPerMin ?? 20,
264
266
  fdLimit: o.fdLimit ?? host!.fdLimit,
265
267
  fdOpen: o.fdOpen ?? host!.fdOpen,
266
268
  dnsServers: o.dnsServers ?? getServers().length,
@@ -404,7 +406,7 @@ export function createGovernor(o: HostedOptions) {
404
406
  return m
405
407
  }
406
408
  const oldest = (m: Map<string, number>) => Math.min(...m.values()) + HOUR
407
- function sites(grp: string, host: string, publish: boolean): Why | null {
409
+ function sites(grp: string, host: string): Why | null {
408
410
  const t = now()
409
411
  const site = host.toLowerCase().replace(/\.$/, '')
410
412
  const net = coarseKey(grp)
@@ -415,9 +417,8 @@ export function createGovernor(o: HostedOptions) {
415
417
  fresh(a, t)
416
418
  fresh(n, t)
417
419
  if (!a.has(site)) {
418
- const max = publish ? 3 * c.sitesPerAddrPerHour : c.sitesPerAddrPerHour
419
- if (a.size >= max) {
420
- return { rule: 'too-many-sites', words: `${max} different sites in an hour from this address${publish ? ' while publishing' : ''}: a new site waits until one ages out; sites already reached keep working`, until: oldest(a) }
420
+ if (a.size >= c.sitesPerAddrPerHour) {
421
+ return { rule: 'too-many-sites', words: `${c.sitesPerAddrPerHour} different sites in an hour from this address: a new site waits until one ages out; sites already reached keep working`, until: oldest(a) }
421
422
  }
422
423
  if (!n.has(site) && n.size >= c.sitesPerNetPerHour) {
423
424
  return { rule: 'too-many-sites', words: `${c.sitesPerNetPerHour} different sites in an hour from this network: a new site waits until one ages out; sites already reached keep working`, until: oldest(n) }
@@ -429,12 +430,16 @@ export function createGovernor(o: HostedOptions) {
429
430
  }
430
431
 
431
432
  // ---- D
432
- 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[] }>()
433
434
  const dialOf = (grp: string) => {
434
435
  let d = dials.get(grp)
435
- 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: [] }))
436
437
  return d
437
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
+ }
438
443
  function dialGate(s: Session): Why | { waitMs: number } | null {
439
444
  const d = dials.get(grpOf(s))
440
445
  if (!d || !d.w) return null
@@ -454,7 +459,7 @@ export function createGovernor(o: HostedOptions) {
454
459
  const grpOf = (s: Session): string => s.hx?.grp ?? s.p.grp
455
460
 
456
461
  /** A session from `ip` (address group `grp`) with this page origin is about to open. */
457
- function admitSession(i: { ip: string; grp: string; origin: string; publish?: boolean; end: (why: Why) => void; tell?: Tell; claimed?: boolean }): { ok: true; adm: Admission } | ({ ok: false } & Why) {
462
+ function admitSession(i: { ip: string; grp: string; origin: string; end: (why: Why) => void; tell?: Tell; claimed?: boolean }): { ok: true; adm: Admission } | ({ ok: false } & Why) {
458
463
  const no = (w: Why) => ({ ok: false as const, ...w })
459
464
  const denied = deniedClient(i.ip, i.origin)
460
465
  if (denied) return no(denied)
@@ -469,7 +474,7 @@ export function createGovernor(o: HostedOptions) {
469
474
  live.add(handle)
470
475
  let done = false
471
476
  const adm: Admission = {
472
- grp: i.grp, publish: !!i.publish, net: coarseKey(i.grp), handle, fair: c.linkBytesPerSec ? new Bucket(Infinity) : null, sent: 0, waiting: false,
477
+ grp: i.grp, net: coarseKey(i.grp), handle, fair: c.linkBytesPerSec ? new Bucket(Infinity) : null, sent: 0, waiting: false,
473
478
  release() {
474
479
  if (done) return
475
480
  done = true
@@ -490,7 +495,7 @@ export function createGovernor(o: HostedOptions) {
490
495
  function admitConnect(s: Session, host: string, _port: number): Why | { waitMs: number } | null {
491
496
  const closed = notAccepting()
492
497
  if (closed) return closed
493
- const crawl = sites(grpOf(s), host, !!s.hx?.publish)
498
+ const crawl = sites(grpOf(s), host)
494
499
  if (crawl) return crawl
495
500
  return dialGate(s)
496
501
  }
@@ -506,15 +511,22 @@ export function createGovernor(o: HostedOptions) {
506
511
  /** D: a dial was refused, found no route, timed out or named a host that does not exist, after `ms`. */
507
512
  function dialFailed(s: Session, ms: number) {
508
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
509
518
  d.w = Math.min(c.dialLimitMs, Math.max(1, Math.round(ms), d.w * 2))
510
519
  }
511
520
  function dialOk(s: Session) {
512
521
  const d = dials.get(grpOf(s))
513
522
  if (!d) return
523
+ if (!d.w) return
514
524
  d.w = Math.floor(d.w / 2)
515
525
  if (!d.w) {
516
- dials.delete(grpOf(s))
526
+ d.gate = 0
517
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))
518
530
  }
519
531
  }
520
532
  const dialWait = (s: Session) => dials.get(grpOf(s))?.w ?? 0
@@ -618,12 +630,13 @@ export function createGovernor(o: HostedOptions) {
618
630
  ticker.unref?.()
619
631
  const saver = o.usageFile ? setInterval(saveUsage, 15_000) : null
620
632
  saver?.unref?.()
621
- const sweep = setInterval(() => {
633
+ function sweep() {
622
634
  const t = now()
623
635
  for (const m of [addrSites, netSites]) for (const [k, x] of m) if (!fresh(x, t).size) m.delete(k)
624
- for (const [k, d] of dials) if (d.gate + HOUR < t) dials.delete(k)
625
- }, 60_000)
626
- 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?.()
627
640
 
628
641
  const state = () => {
629
642
  roll()
@@ -686,6 +699,7 @@ export function createGovernor(o: HostedOptions) {
686
699
  bytes,
687
700
  limitWhy,
688
701
  tick,
702
+ sweep,
689
703
  slowed,
690
704
  take,
691
705
  waitMs,
@@ -697,7 +711,7 @@ export function createGovernor(o: HostedOptions) {
697
711
  (c.monthBytes ? `; ${Math.round(c.monthBytes / 1e9)} GB a month, heavy users slowed first when the month runs ahead` : ''),
698
712
  stop() {
699
713
  clearInterval(ticker)
700
- clearInterval(sweep)
714
+ clearInterval(sweeper)
701
715
  if (saver) clearInterval(saver)
702
716
  saveUsage()
703
717
  },
package/relay/main.ts CHANGED
@@ -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'])
package/relay/relay.ts CHANGED
@@ -132,7 +132,6 @@ export function createRelay(o: RelayOptions) {
132
132
  const r = health(url.pathname)
133
133
  return req.method === 'HEAD' ? new Response(null, { status: r.status, headers: r.headers }) : r // same status and headers, no body
134
134
  }
135
- if (gov && wisp && url.pathname === '/publish-token') return wisp.publishToken(req, srv, url)
136
135
  if (url.pathname === '/relay') {
137
136
  const protos = req.headers.get('sec-websocket-protocol') || ''
138
137
  if (wisp && protos.includes('wisp-v2') && !protos.includes(PROTOCOL)) return new Response(`this is the wmd-relay.v1 endpoint; Wisp is at ${wisp.path}\n`, { status: 400 })
package/relay/token.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // P-B10 relay: session tokens. A token is "v1.<payload>.<mac>", base64url without padding,
2
- // where the payload is JSON {sid, exp (ms since the epoch), uid?, grp?, kind?} and the mac is
2
+ // where the payload is JSON {sid, exp (ms since the epoch), uid?, grp?} and the mac is
3
3
  // HMAC-SHA256 of "v1.<payload>" with the relay's secret. Whoever serves the page
4
4
  // (test/serve.ts, or wmn's server) mints them with the same secret (TokenIssuer below); the
5
5
  // browser presents one as a WebSocket subprotocol "t.<token>" (not in the URL, so it stays
@@ -9,8 +9,6 @@
9
9
  // uid the guest identity the relay keeps caps for, across reconnects and fresh tokens
10
10
  // (one guest machine of one page); defaults to sid
11
11
  // grp the client the issuer saw (an address, a user): the relay caps sessions per grp
12
- // kind "publish": a publish identity (TokenIssuer.issuePublish), which a hosted relay gives a higher sites-an-hour
13
- // count (hosted.ts). Only the signature makes it one: the relay never takes a client's word for it
14
12
  //
15
13
  // Parsing is strict: a token is exactly one encoding of its payload and MAC, so the same
16
14
  // token never verifies under two spellings.
@@ -21,20 +19,19 @@ const b64u = (b: Buffer) => b.toString('base64url')
21
19
  const B64U = /^[A-Za-z0-9_-]+$/
22
20
  const ID = /^[A-Za-z0-9_.:\/-]{1,128}$/
23
21
 
24
- export type TokenFields = { sid?: string; uid?: string; grp?: string; kind?: string; ttlMs?: number; now?: number }
22
+ export type TokenFields = { sid?: string; uid?: string; grp?: string; ttlMs?: number; now?: number }
25
23
 
26
- export function mintToken(secret: string, { sid = b64u(randomBytes(12)), uid, grp, kind, ttlMs = 60 * 60_000, now = Date.now() }: TokenFields = {}): string {
24
+ export function mintToken(secret: string, { sid = b64u(randomBytes(12)), uid, grp, ttlMs = 60 * 60_000, now = Date.now() }: TokenFields = {}): string {
27
25
  if (!secret || secret.length < 16) throw new Error('relay secret: at least 16 characters')
28
26
  const body: Record<string, unknown> = { sid, exp: now + ttlMs }
29
27
  if (uid) body.uid = uid
30
28
  if (grp) body.grp = grp
31
- if (kind) body.kind = kind
32
29
  const payload = b64u(Buffer.from(JSON.stringify(body)))
33
30
  const mac = createHmac('sha256', secret).update(`v1.${payload}`).digest()
34
31
  return `v1.${payload}.${b64u(mac)}`
35
32
  }
36
33
 
37
- export type TokenInfo = { sid: string; exp: number; uid?: string; grp?: string; kind?: 'publish' }
34
+ export type TokenInfo = { sid: string; exp: number; uid?: string; grp?: string }
38
35
 
39
36
  /** The token's session, or an error string. */
40
37
  export function verifyToken(secret: string, token: string, now = Date.now()): TokenInfo | string {
@@ -55,9 +52,8 @@ export function verifyToken(secret: string, token: string, now = Date.now()): To
55
52
  }
56
53
  if (typeof info !== 'object' || !info || typeof info.sid !== 'string' || !ID.test(info.sid) || typeof info.exp !== 'number') return 'bad token'
57
54
  for (const k of ['uid', 'grp'] as const) if (info[k] !== undefined && (typeof info[k] !== 'string' || !ID.test(info[k]!))) return 'bad token'
58
- if (info.kind !== undefined && info.kind !== 'publish') return 'bad token'
59
55
  if (info.exp <= now) return 'token expired'
60
- return { sid: info.sid, exp: info.exp, uid: info.uid, grp: info.grp, ...(info.kind ? { kind: info.kind } : {}) }
56
+ return { sid: info.sid, exp: info.exp, uid: info.uid, grp: info.grp }
61
57
  }
62
58
 
63
59
  /** The key a client's limits are kept under: IPv6 addresses by their /64 (one subscriber holds 2^64 of them). */
@@ -83,7 +79,6 @@ export function clientKey(client: string): string {
83
79
  * an hour. At most `maxClients` clients are remembered (the least recently used go first).
84
80
  */
85
81
  export const MIN_TTL_MS = 60_000
86
- export const PUBLISH_TTL_MS = 60 * 60_000
87
82
  export class TokenIssuer {
88
83
  perMinute: number
89
84
  instancesPerHour: number
@@ -91,7 +86,6 @@ export class TokenIssuer {
91
86
  maxClients: number
92
87
  minTtlMs: number
93
88
  clients = new Map<string, { times: number[]; instances: Map<string, number> }>() // least recently used first
94
- publishing = new Map<string, { instance: string; token: string; until: number }>() // per client: its live publish identity, in the order they end
95
89
  constructor(public secret: string, { perMinute = 60, instancesPerHour = 20, maxTtlMs = 60 * 60_000, maxClients = 10_000, minTtlMs = MIN_TTL_MS } = {}) {
96
90
  this.perMinute = perMinute
97
91
  this.instancesPerHour = instancesPerHour
@@ -119,27 +113,4 @@ export class TokenIssuer {
119
113
  const ttl = Math.min(Math.max(ttlMs, this.minTtlMs), this.maxTtlMs)
120
114
  return { token: mintToken(this.secret, { uid: `${key}/${instance}`, grp: key, ttlMs: ttl, now }) }
121
115
  }
122
- /** A publish identity for this client, for one hour; at most one per client at a time. The page that holds it gets the
123
- same token again (a reconnect); another page of the client is refused until its hour is over. When `maxClients`
124
- identities are live, a new one is refused (503) until the first ends: a live one is never dropped. */
125
- issuePublish(client: string, instance: string, now = Date.now()): { token: string; until: number } | { error: string; status: number; until?: number } {
126
- if (!ID.test(client)) return { error: 'bad client', status: 400 }
127
- if (!/^[A-Za-z0-9_-]{8,64}$/.test(instance || '')) return { error: 'instance: 8-64 characters [A-Za-z0-9_-]', status: 400 }
128
- const key = clientKey(client)
129
- for (const [k, p] of this.publishing) {
130
- if (p.until > now) break
131
- this.publishing.delete(k)
132
- }
133
- const live = this.publishing.get(key)
134
- if (live && live.instance === instance) return { token: live.token, until: live.until }
135
- if (live) return { error: `this address already has a publish identity; another can be had in ${Math.ceil((live.until - now) / 60_000)} min`, status: 429, until: live.until }
136
- if (this.publishing.size >= this.maxClients) {
137
- const first = this.publishing.values().next().value!
138
- return { error: `the relay is giving out as many publish identities as it can hold; try again in ${Math.ceil((first.until - now) / 60_000)} min`, status: 503, until: first.until }
139
- }
140
- const until = now + PUBLISH_TTL_MS
141
- const token = mintToken(this.secret, { uid: `${key}/publish.${instance}`, grp: key, kind: 'publish', ttlMs: PUBLISH_TTL_MS, now })
142
- this.publishing.set(key, { instance, token, until })
143
- return { token, until }
144
- }
145
116
  }
package/relay/wisp.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  // W59: the Wisp v2 adapter (src/net/wisp.js, docs/wisp-wmd-extensions.md) over the relay engine.
2
2
  //
3
3
  // Who may use it (plan §5):
4
- // - token: the HMAC `t.<token>` subprotocol, as wmd-relay.v1 (wmn's bundled relay); on a hosted relay, the publish
5
- // identity it issues itself (relay.ts /publish-token), used only from the address it was given to
4
+ // - token: the HMAC `t.<token>` subprotocol, as wmd-relay.v1 (wmn's bundled relay)
6
5
  // - open: no key (the standalone relay's default); identity = the client's instance id (0xE7)
7
6
  // plus its address group, bandwidth shared per address
8
7
  // - keys (optional): Wisp password auth, scrypt hashes; a relay with keys refuses keyless clients
@@ -24,7 +23,7 @@ import { type AllowEntry, type Engine, type Principal, type Session, type Shared
24
23
  import { type Governor, type Why, resolveClient } from './hosted.ts'
25
24
  import { type OwnerKey, verifySecret } from './keys.ts'
26
25
  import type { Cidr } from './policy.ts'
27
- import { TokenIssuer, clientKey, verifyToken } from './token.ts'
26
+ import { clientKey, verifyToken } from './token.ts'
28
27
 
29
28
  export type WispOptions = {
30
29
  enabled?: boolean
@@ -185,32 +184,6 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
185
184
  return reply(body, status)
186
185
  }
187
186
 
188
- const issuer = w.hosted && o.secret ? new TokenIssuer(o.secret) : null
189
-
190
- /** Hosted: GET /publish-token?instance=ID from a listed page gives {token, until}, a publish identity for this address
191
- (token.ts issuePublish), or {error, until?} in words. The page's origin may read the answer. */
192
- function publishToken(req: Request, srv: any, url: URL): Response {
193
- const origin = req.headers.get('origin') || ''
194
- if (!issuer || origin.length > 256 || !w.hosted!.siteAllowed(origin)) {
195
- log({ ev: 'publish-token-refused', why: 'not-allowed-site' })
196
- return reply('this page is not one of the sites this relay serves\n', 403)
197
- }
198
- const headers = { 'access-control-allow-origin': origin, vary: 'Origin', 'cache-control': 'no-store' }
199
- if (req.method !== 'GET') return new Response(`GET only\n${w.hosted!.contactLine}`, { status: 405, headers })
200
- const ip = clientIp(req, srv)
201
- if (typeof ip !== 'string') {
202
- for (const [k, v] of Object.entries(headers)) ip.headers.set(k, v)
203
- return ip
204
- }
205
- const r = issuer.issuePublish(ip, url.searchParams.get('instance') || '')
206
- if ('token' in r) {
207
- log({ ev: 'publish-token', addr: clientKey(ip), origin })
208
- return Response.json(r, { headers })
209
- }
210
- log({ ev: 'publish-token-refused', why: r.error, addr: clientKey(ip) })
211
- return Response.json({ error: r.error, ...(r.until ? { until: r.until } : {}), abuseContact: w.hosted!.contact }, { status: r.status, headers })
212
- }
213
-
214
187
  async function fetch(req: Request, srv: any, url: URL): Promise<Response | undefined> {
215
188
  const ip = clientIp(req, srv)
216
189
  if (typeof ip !== 'string') return ip
@@ -237,13 +210,11 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
237
210
  let uid = ''
238
211
  let grp = ''
239
212
  let exp = Infinity
240
- let publish = false
241
213
  let p: Principal | null = null
242
214
  const tok = protos.find(x => x.startsWith('t.'))
243
215
  if (tok && o.secret) {
244
216
  const info = verifyToken(o.secret, tok.slice(2))
245
217
  if (typeof info === 'string') return refuse(401, `${info}\n`, info, { origin })
246
- if (w.hosted && info.grp !== addr) return refuse(401, 'this token was given to another address\n', 'token of another address', { addr })
247
218
  if (engine.hasSession(info.sid)) {
248
219
  stats.refusedSessions++
249
220
  return reply('session already connected\n', 409)
@@ -253,9 +224,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
253
224
  uid = info.uid || info.sid
254
225
  grp = info.grp || uid
255
226
  exp = info.exp
256
- publish = info.kind === 'publish'
257
- // a hosted publish session shares its address's bandwidth with the address's open sessions
258
- const a = engine.admit(uid, grp, exp, { proto: 'wisp' }, w.hosted && publish ? sharedFor(grp) : undefined)
227
+ const a = engine.admit(uid, grp, exp, { proto: 'wisp' })
259
228
  if (!a.ok) return reply(a.body, a.status)
260
229
  p = a.p
261
230
  } else if (rest) {
@@ -302,7 +271,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
302
271
  const full = w.hosted?.claimHandshake() ?? null
303
272
  const headers: Record<string, string> = {}
304
273
  if (v2 && protos.length) headers['Sec-WebSocket-Protocol'] = protos.includes(SUBPROTOCOL) ? SUBPROTOCOL : protos[0]
305
- const data = { kind: 'wisp', v2, mode, addr, ip, sid, uid, grp, exp, publish, p, key, origin, hs: null as any, hsTimer: null as any, pre, claimed: !!w.hosted && !full, refuse: full as Why | null, at: performance.now() }
274
+ const data = { kind: 'wisp', v2, mode, addr, ip, sid, uid, grp, exp, p, key, origin, hs: null as any, hsTimer: null as any, pre, claimed: !!w.hosted && !full, refuse: full as Why | null, at: performance.now() }
306
275
  const ok = srv.upgrade(req, Object.keys(headers).length ? { data, headers } : { data }) // Bun refuses an empty headers object
307
276
  if (!ok) {
308
277
  release(data)
@@ -448,7 +417,7 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
448
417
  tell(REASON_KIND.REFUSED, why.rule, why.until ?? 0, why.words)
449
418
  ws.close(4011, why.words.slice(0, 120))
450
419
  }
451
- adm = w.hosted.admitSession({ ip: d.ip, grp: d.grp, origin: d.origin, publish: d.publish, end, tell, claimed: d.claimed })
420
+ adm = w.hosted.admitSession({ ip: d.ip, grp: d.grp, origin: d.origin, end, tell, claimed: d.claimed })
452
421
  if (!adm.ok) return refuseHs(ws, client, adm)
453
422
  d.claimed = false // the session holds the socket's count now
454
423
  d.rel = adm.adm.release
@@ -695,7 +664,6 @@ export function createWisp(engine: Engine, o: { secret?: string; port: () => num
695
664
  ws.close(1013, 'the relay is full')
696
665
  },
697
666
  fetch,
698
- publishToken,
699
667
  open: open_,
700
668
  message,
701
669
  close,
package/src/blob.js ADDED
@@ -0,0 +1,12 @@
1
+ // D-554: WebKit (Playwright's WebKit 26 here) cannot read a Blob of 128 MiB or more whole: arrayBuffer() and stream()
2
+ // fail with NotReadableError ("The I/O read operation failed."). Slices of the same Blob read. Measured with 128, 256
3
+ // and 400 MiB Blobs; 64 MiB read whole. Readers of large slices (a tar entry, a layer) go through here too.
4
+ const SLICE = 16 << 20
5
+
6
+ /** A Blob's bytes, read in 16 MiB slices. */
7
+ export async function blobBytes(b) {
8
+ if (b.size <= SLICE) return new Uint8Array(await b.arrayBuffer())
9
+ const out = new Uint8Array(b.size)
10
+ for (let o = 0; o < b.size; o += SLICE) out.set(new Uint8Array(await b.slice(o, o + SLICE).arrayBuffer()), o)
11
+ return out
12
+ }