nervur 0.20.1 → 0.20.3

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 (131) hide show
  1. package/README.md +174 -12
  2. package/dist/browser/index.d.ts +35 -0
  3. package/dist/browser/index.js +188 -0
  4. package/dist/browser/worker.d.ts +12 -0
  5. package/dist/browser/worker.js +40 -0
  6. package/dist/browser.d.ts +1 -0
  7. package/dist/browser.js +7 -0
  8. package/dist/cli/command.d.ts +1 -1
  9. package/dist/cli/command.js +84 -20
  10. package/dist/cli/harbor.d.ts +7 -12
  11. package/dist/cli/harbor.js +73 -123
  12. package/dist/contract/index.d.ts +2 -1
  13. package/dist/contract/index.js +3 -7
  14. package/dist/edge/index.d.ts +71 -0
  15. package/dist/edge/index.js +334 -0
  16. package/dist/edge.d.ts +1 -0
  17. package/dist/edge.js +8 -0
  18. package/dist/folder/index.d.ts +4 -0
  19. package/dist/folder/index.js +57 -14
  20. package/dist/harbor/carrying.d.ts +17 -0
  21. package/dist/harbor/carrying.js +38 -0
  22. package/dist/harbor/catalogue.d.ts +2 -0
  23. package/dist/harbor/catalogue.js +28 -20
  24. package/dist/harbor/dock.d.ts +22 -3
  25. package/dist/harbor/dock.js +69 -13
  26. package/dist/harbor/harbor.d.ts +14 -4
  27. package/dist/harbor/harbor.js +144 -15
  28. package/dist/harbor/index.d.ts +6 -3
  29. package/dist/harbor/index.js +4 -1
  30. package/dist/harbor/package.d.ts +10 -0
  31. package/dist/harbor/package.js +52 -1
  32. package/dist/harbor/probe.d.ts +18 -0
  33. package/dist/harbor/probe.js +15 -0
  34. package/dist/harbor/registry.d.ts +1 -0
  35. package/dist/harbor/registry.js +11 -0
  36. package/dist/harbor/relay.d.ts +15 -0
  37. package/dist/harbor/relay.js +406 -0
  38. package/dist/harbor/root-line.js +4 -2
  39. package/dist/harbor/terrain.d.ts +12 -0
  40. package/dist/harbor/terrain.js +19 -0
  41. package/dist/http/index.d.ts +4 -0
  42. package/dist/http/index.js +156 -0
  43. package/dist/http/websocket.d.ts +29 -0
  44. package/dist/http/websocket.js +133 -0
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.js +8 -2
  47. package/dist/line/answer.d.ts +8 -0
  48. package/dist/line/answer.js +60 -0
  49. package/dist/line/frame.d.ts +34 -0
  50. package/dist/line/frame.js +81 -0
  51. package/dist/line/ground.d.ts +3 -0
  52. package/dist/line/ground.js +9 -0
  53. package/dist/line/index.d.ts +5 -0
  54. package/dist/line/index.js +9 -0
  55. package/dist/line/push.d.ts +7 -0
  56. package/dist/line/push.js +92 -0
  57. package/dist/line/web.d.ts +30 -0
  58. package/dist/line/web.js +181 -0
  59. package/dist/node/index.d.ts +36 -0
  60. package/dist/node/index.js +228 -0
  61. package/dist/node.d.ts +1 -0
  62. package/dist/node.js +7 -0
  63. package/dist/pointer/bodies.d.ts +2 -0
  64. package/dist/pointer/bodies.js +10 -0
  65. package/dist/pointer/world.d.ts +7 -1
  66. package/dist/pointer/world.js +6 -0
  67. package/dist/quo/address.d.ts +19 -0
  68. package/dist/quo/address.js +84 -0
  69. package/dist/quo/door.d.ts +1 -1
  70. package/dist/quo/door.js +4 -2
  71. package/dist/quo/index.d.ts +1 -0
  72. package/dist/quo/index.js +1 -0
  73. package/dist/quo/invitation.d.ts +1 -0
  74. package/dist/quo/invitation.js +9 -5
  75. package/dist/tcp/frame.d.ts +0 -4
  76. package/dist/tcp/frame.js +0 -10
  77. package/dist/tcp/index.d.ts +8 -2
  78. package/dist/tcp/index.js +123 -27
  79. package/dist/ward/index.d.ts +2 -2
  80. package/dist/ward/index.js +1 -1
  81. package/dist/ward/stance.d.ts +2 -0
  82. package/dist/ward/stance.js +3 -1
  83. package/dist/ward/ward-being.d.ts +2 -1
  84. package/dist/ward/ward-being.js +1 -1
  85. package/dist/ward/ward.d.ts +7 -0
  86. package/dist/ward/ward.js +6 -0
  87. package/package.json +21 -2
  88. package/src/browser/index.ts +207 -0
  89. package/src/browser/worker.ts +66 -0
  90. package/src/browser.ts +9 -0
  91. package/src/cli/command.ts +74 -20
  92. package/src/cli/harbor.ts +73 -116
  93. package/src/contract/index.ts +11 -8
  94. package/src/edge/index.ts +413 -0
  95. package/src/edge.ts +10 -0
  96. package/src/folder/index.ts +57 -12
  97. package/src/harbor/carrying.ts +56 -0
  98. package/src/harbor/catalogue.ts +32 -17
  99. package/src/harbor/dock.ts +88 -17
  100. package/src/harbor/harbor.ts +146 -17
  101. package/src/harbor/index.ts +6 -3
  102. package/src/harbor/package.ts +55 -1
  103. package/src/harbor/probe.ts +34 -0
  104. package/src/harbor/registry.ts +11 -0
  105. package/src/harbor/relay.ts +418 -0
  106. package/src/harbor/root-line.ts +4 -1
  107. package/src/harbor/terrain.ts +28 -2
  108. package/src/http/index.ts +150 -0
  109. package/src/http/websocket.ts +124 -0
  110. package/src/index.ts +35 -2
  111. package/src/line/answer.ts +59 -0
  112. package/src/line/frame.ts +88 -0
  113. package/src/line/ground.ts +19 -0
  114. package/src/line/index.ts +9 -0
  115. package/src/line/push.ts +112 -0
  116. package/src/line/web.ts +227 -0
  117. package/src/node/index.ts +234 -0
  118. package/src/node.ts +9 -0
  119. package/src/pointer/bodies.ts +11 -0
  120. package/src/pointer/world.ts +18 -2
  121. package/src/quo/address.ts +87 -0
  122. package/src/quo/door.ts +4 -2
  123. package/src/quo/index.ts +1 -0
  124. package/src/quo/invitation.ts +10 -6
  125. package/src/stand/stand.ts +60 -21
  126. package/src/tcp/index.ts +142 -31
  127. package/src/ward/index.ts +2 -2
  128. package/src/ward/stance.ts +6 -1
  129. package/src/ward/ward-being.ts +2 -2
  130. package/src/ward/ward.ts +16 -0
  131. package/src/tcp/frame.ts +0 -78
package/README.md CHANGED
@@ -18,6 +18,76 @@ Node 22.18 or later. The main entry imports no platform, so it runs in a
18
18
  browser, Deno, Bun and workerd as well. `nervur/folder` keeps a harbor on
19
19
  a disk and needs Node's file system.
20
20
 
21
+ ## A harbor where you run
22
+
23
+ ```js
24
+ import { Harbor } from 'nervur';
25
+ import * as greetings from './greetings.js';
26
+
27
+ const harbor = await Harbor.open({ modules: [greetings] });
28
+ ```
29
+
30
+ With no terrain, the harbor picks its ground's bodies itself. In Node it
31
+ keeps itself in a folder, `where` if you name one, else `$NERVUR_DIR`,
32
+ else `~/.nervur`, speaks TCP and, where you open them, the web and Web
33
+ Push, and serves its root line, so the `nervur` command below asks it
34
+ while it stands. A folder opens in one run at a time. Bundled for a
35
+ browser, in a page or a worker, it keeps itself in the IndexedDB
36
+ database `where` names, else `nervur`, its seed sealed under a key the
37
+ browser never hands out, speaks the web, and opens in one run at a time.
38
+ On Cloudflare Workers it is one Durable Object, answering Quo at `/quo`
39
+ on whatever host reaches it, and asking over TCP and the web. Anywhere
40
+ else it lives as long as its process and speaks the web. Hand in a
41
+ terrain, as below, and that one is used.
42
+
43
+ ## A harbor on the edge
44
+
45
+ An edge harbor is born on your machine and carried there, so you pilot it
46
+ from your own harbor, as you pilot one on a server. Make it and set it
47
+ up with the command below, naming where the edge will reach it:
48
+
49
+ ```sh
50
+ nervur init --dir ./edge
51
+ nervur reach https://harbor.example.workers.dev/quo --dir ./edge
52
+ nervur own pilot --dir ./edge
53
+ nervur edge pack ./package.json --dir ./edge
54
+ ```
55
+
56
+ `own` prints the owner invitation, which never leaves your machine.
57
+ `edge pack` prints the harbor's seed and writes its sealed package. Keep
58
+ the seed as the worker's secret, `wrangler secret put NERVUR_SEED`, and
59
+ bundle the package with the worker:
60
+
61
+ ```js
62
+ import { harborObject } from 'nervur/edge';
63
+ import carried from './package.json';
64
+ import * as greetings from './greetings.js';
65
+
66
+ export const Harbor = harborObject({ modules: [greetings], package: carried });
67
+ ```
68
+
69
+ The first time the object starts, the package lands in its storage and
70
+ the harbor opens under that seed; after that the storage is the harbor,
71
+ and a new deploy leaves it be. Then, from your own harbor:
72
+
73
+ ```sh
74
+ nervur pilot edge '<the owner invitation>' --dir ./mine
75
+ nervur host shop --via edge --dir ./mine
76
+ ```
77
+
78
+ `pilot` opens the web carrier where the invitation names a web address.
79
+ An object with no package draws its own seed, sealed under the worker's
80
+ `NERVUR_SECRET`, and `stood` runs the worker's own code on it as root:
81
+
82
+ ```js
83
+ export const Shop = harborObject({
84
+ modules: [greetings],
85
+ stood: async (harbor) => {
86
+ // runs once the harbor stands
87
+ },
88
+ });
89
+ ```
90
+
21
91
  ## A world in one process
22
92
 
23
93
  ```js
@@ -116,13 +186,33 @@ stands a ward under a seed you name, or a drawn one.
116
186
  - **Harbor, Terrain, Dock, Catalogue, Registry, WardBeing.** The onion.
117
187
  - **World, PointerTerrain** and the pointer bodies, `HeldLoader` among
118
188
  them. A whole world with this package alone.
189
+ - **addGround, grounds**: the grounds `Harbor.open()` tries, and a way to
190
+ add your own.
191
+ - **NodeTerrain**, from `nervur/node`: the Node ground's terrain, for a
192
+ folder you name in code.
193
+ - **BrowserTerrain, IdbMemory, IdbCustody**, from `nervur/browser`: the
194
+ browser ground's terrain and bodies. `serveWorker(Harbor.open())` at
195
+ the top of a shared or service worker holds one harbor for every tab,
196
+ each tab asks it with `rootOf(worker)`, and a push the service worker
197
+ hears makes every relay client `ring`.
119
198
  - **FolderMemory, FolderCustody**, from `nervur/folder`.
120
199
  - **TcpCarrier, TcpListener**, from `nervur/tcp`: Quo over TCP. Route a
121
- ward pk to `host:port` on the carrier, hand the listener your harbor,
122
- and harbors in two processes, or a harbor and another kit, speak.
123
-
124
- Other carriers, storage beyond a folder, key custody, and every screen
125
- are `@nervur-org/dock`'s, as classes fulfilling these contracts.
200
+ ward pk to its `tcp://host:port` addresses on the carrier, hand the
201
+ listener your harbor, and harbors in two processes, or a harbor and
202
+ another kit, speak. `tcpFaculty` is the same carrier as a faculty of a
203
+ harbor.
204
+ - **WebDialer, webFaculty**: Quo over the web, a post to `https://` or a
205
+ held line to `wss://`, in any engine with `fetch` and WebSocket.
206
+ `webServe` and `webGround`, from `nervur/http`, listen for it on Node.
207
+
208
+ - **The relay**, `org.nervur.relay` and `org.nervur.relay-client`, in
209
+ every harbor: a harbor nobody can dial is reached through one that
210
+ can, by relations alone. See "A harbor nobody dials" below.
211
+ - **Push, webPush**: a ring with no content, as Web Push sends it,
212
+ signed with VAPID, on fetch and Web Crypto.
213
+
214
+ Carriers such as MCP, and every screen, are `@nervur-org/*`'s, as
215
+ classes fulfilling these contracts.
126
216
 
127
217
  ## The command
128
218
 
@@ -132,33 +222,50 @@ folder, Quo over TCP as its carrier. A module is a file that exports
132
222
 
133
223
  ```sh
134
224
  nervur init --dir ./harbor
135
- nervur module add ./greetings.js --dir ./harbor
225
+ nervur module add ./greetings.js --dir ./harbor # or @acme/greetings, or its id
136
226
  nervur serve --dir ./harbor --port 7000
227
+ nervur reach tcp://harbor.example:7000 --dir ./harbor
228
+ nervur open web org.nervur.web --dir ./harbor
229
+ nervur ask web listen '{"port":8080,"path":"/quo"}' --dir ./harbor
137
230
  nervur host alice --dir ./harbor
138
231
  nervur boot --ward alice greeter org.example.greeter --dir ./harbor
139
- nervur route <ward pk> host:port --dir ./harbor
140
232
  nervur ask --ward alice greeter hello --dir ./harbor
141
233
  ```
142
234
 
235
+ `reach` names where the harbor is reached, and every invitation it gives
236
+ carries those addresses in `at`. With no `reach`, an invitation carries
237
+ where the harbor listens, which serves a harbor callers reach directly;
238
+ behind a router or a proxy, name the public address with `reach`. A
239
+ harbor that takes such an invitation keeps them as that ward's route,
240
+ so nobody routes it by hand. Where an invitation carries no `at`,
241
+ `nervur route <ward pk> tcp://host:port` names the route, and a route
242
+ named this way is trusted first.
243
+
143
244
  `init` makes the harbor and prints its pk and an owner invitation. `serve`
144
245
  listens for Quo on TCP and for the root's asks on a local socket, open to
145
246
  you alone. Every other command is one root ask, sent to the served
146
247
  harbor, or answered from the folder when none is served. Each prints one
147
- JSON line. `--dir` defaults to `$NERVUR_DIR`, then `~/.nervur`. The
148
- catalogue keeps a module by its file's absolute path, and routes live in
149
- the harbor like everything else, so a restart finds both. `nervur
248
+ JSON line. `--dir` defaults to `$NERVUR_DIR`, then `~/.nervur`.
249
+ `module add` takes a file, an installed package, or a module's id, and
250
+ the catalogue keeps the id the module declares, so the same harbor finds
251
+ its code on any ground: on this machine through `modules.json` beside
252
+ the harbor, which remembers where each id was found, and on an edge or
253
+ in a browser in the bundle. Routes live in the harbor like everything
254
+ else, so a restart finds both. `nervur
150
255
  modules` shows what runs, and `nervur module remove <module>` stops one.
151
256
 
152
257
  A harbor elsewhere is piloted with the owner invitation its `init`
153
258
  printed, from a harbor of your own:
154
259
 
155
260
  ```sh
156
- nervur pilot far '<owner invitation>' host:port --dir ./mine
261
+ nervur pilot far '<owner invitation>' --dir ./mine
157
262
  nervur host shop --via far --dir ./mine
158
263
  ```
159
264
 
160
265
  `pilot` boots an `org.nervur.pilot` being in your harbor that holds the
161
- invitation as an ordinary Quo relation, kept in your harbor's memory. With
266
+ invitation as an ordinary Quo relation, kept in your harbor's memory, and
267
+ reaches the far harbor where its invitation's `at` says; where it says
268
+ nothing, name the address after the invitation. With
162
269
  `--via far`, any command is asked of the far harbor as its owner, sealed
163
270
  over TCP, except `own` and `disown`, which stay the far root's.
164
271
  `nervur unboot far` lets it go.
@@ -166,6 +273,61 @@ over TCP, except `own` and `disown`, which stay the far root's.
166
273
  `rootLine(harbor, request)` is the same request in code, for any other
167
274
  front: `{ ward?, method?, args? }` in, one JSON answer out.
168
275
 
276
+ ## A harbor nobody dials
277
+
278
+ A device behind a home router listens nowhere a caller can reach. A
279
+ served harbor that can be reached relays for it. On the relay's harbor:
280
+
281
+ ```sh
282
+ nervur open relay org.nervur.relay --dir ./relay
283
+ nervur ask relay invite '{"id":"house","wards":["<camera ward pk>"]}' --dir ./relay
284
+ ```
285
+
286
+ On the device, with the relay's address as its own reach:
287
+
288
+ ```sh
289
+ nervur open client org.nervur.relay-client --dir ./house
290
+ nervur ask client hold '{"invitation":<the invitation>,"line":true}' --dir ./house
291
+ nervur reach tcp://relay.example:7000 --dir ./house
292
+ nervur serve --dir ./house
293
+ ```
294
+
295
+ Every invitation the device gives now sends callers to the relay. The
296
+ relay holds each sealed box, the client collects it by an ordinary ask,
297
+ hands it to the device's door, and returns the sealed reply in its next
298
+ ask. Nothing is opened on the way. `line` keeps collecting, for a
299
+ camera. Without it the client is a doorbell: `ring` collects what waits,
300
+ and `"every": 60000` rings on a schedule, for a garage door.
301
+
302
+ A doorbell can also be rung by a push with nothing in it. The relay's
303
+ harbor opens Web Push and names who it is:
304
+
305
+ ```sh
306
+ nervur open push org.nervur.web-push --dir ./relay
307
+ nervur ask push subject '{"subject":"mailto:ops@example.org"}' --dir ./relay
308
+ ```
309
+
310
+ The device asks its client for the key (`nervur ask client key`),
311
+ subscribes to pushes with it, and hands the subscription's endpoint to
312
+ `nervur ask client bell '{"endpoint":"https://..."}'`. When a box waits
313
+ and no line collects, the relay rings that endpoint once. Whatever hears
314
+ the push on the device asks the client's `ring`.
315
+
316
+ ## Moving a harbor
317
+
318
+ A harbor moves to another machine by its bytes, with no harbor open:
319
+
320
+ ```sh
321
+ nervur export --with-seed --dir ./harbor > harbor.json
322
+ nervur import harbor.json --dir ./elsewhere
323
+ ```
324
+
325
+ `export` reads the folder's sealed places, and carries the seed only
326
+ where you ask for it. `import` writes them into a folder that holds none.
327
+ A package with no seed lands where custody already holds the one it was
328
+ sealed under. In code, `copyPackage(from, to)` carries every place of one
329
+ memory into another.
330
+
169
331
  ## License
170
332
 
171
333
  Apache-2.0.
@@ -0,0 +1,35 @@
1
+ import { Custody, Memory, type Entropy, type Module } from '../contract/index.ts';
2
+ import type { GroundProbe } from '../harbor/index.ts';
3
+ import { PointerTerrain } from '../pointer/index.ts';
4
+ export { ringAll, rootOf, serveWorker, type Reaching } from './worker.ts';
5
+ export declare const BROWSER = "browser";
6
+ export declare const BROWSER_HARBOR = "nervur";
7
+ export declare class IdbMemory extends Memory {
8
+ #private;
9
+ constructor(name?: string);
10
+ get name(): string;
11
+ read(place: string): Promise<Map<string, Uint8Array>>;
12
+ write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void>;
13
+ protected put(store: IDBObjectStore, key: [string, string], bytes: ArrayBuffer | null): void;
14
+ places(): Promise<string[]>;
15
+ forget(place: string): Promise<void>;
16
+ close(): Promise<void>;
17
+ }
18
+ export declare class IdbCustody extends Custody {
19
+ #private;
20
+ constructor(name?: string, entropy?: Entropy);
21
+ seed(): Promise<Uint8Array>;
22
+ close(): Promise<void>;
23
+ }
24
+ export type BrowserParts = {
25
+ readonly where?: string;
26
+ readonly modules?: readonly Module[];
27
+ };
28
+ export declare class BrowserTerrain extends PointerTerrain {
29
+ #private;
30
+ readonly where: string;
31
+ constructor(parts?: BrowserParts);
32
+ claim(): Promise<void>;
33
+ release(): Promise<void>;
34
+ }
35
+ export declare const browserGround: GroundProbe;
@@ -0,0 +1,188 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur/browser`: the browser ground, in a page or a worker. A harbor
3
+ // keeps its sealed memory and its seed in one IndexedDB database, named
4
+ // by `where`, speaks the web, and holds its database to one run with a
5
+ // Web Lock of the same name.
6
+ //
7
+ // entries one sealed entry per key [place, name]
8
+ // custody the seed, sealed under an AES-GCM key the engine never
9
+ // hands out, itself kept as the engine keeps a CryptoKey
10
+ //
11
+ // A keep is one transaction, so it is whole or nothing by IndexedDB's own
12
+ // rule.
13
+ import { Custody, Memory } from '../contract/index.js';
14
+ import { webFaculty, WEB_OPENING, webPush } from '../line/index.js';
15
+ import { CryptoEntropy, HeldLoader, PointerTerrain } from '../pointer/index.js';
16
+ export { ringAll, rootOf, serveWorker } from './worker.js';
17
+ export const BROWSER = 'browser';
18
+ // The database a harbor keeps itself in unless named.
19
+ export const BROWSER_HARBOR = 'nervur';
20
+ const ENTRIES = 'entries';
21
+ const CUSTODY = 'custody';
22
+ const SEED = 'seed';
23
+ // Past every lowercase hex name, so [place, END] bounds a place's keys.
24
+ const END = '￿';
25
+ const done = (request) => new Promise((resolve, reject) => {
26
+ request.onsuccess = () => resolve(request.result);
27
+ request.onerror = () => reject(request.error ?? new Error('IndexedDB refused'));
28
+ });
29
+ // A transaction's end. A request that fails aborts it, and the abort
30
+ // carries that request's error.
31
+ const finished = (tx) => new Promise((resolve, reject) => {
32
+ tx.oncomplete = () => resolve();
33
+ tx.onabort = () => reject(tx.error ?? new Error('the keep was aborted'));
34
+ });
35
+ // One database, opened once per body and made on first open.
36
+ class Database {
37
+ name;
38
+ #open;
39
+ constructor(name) {
40
+ this.name = name;
41
+ }
42
+ get() {
43
+ this.#open ??= new Promise((resolve, reject) => {
44
+ const request = indexedDB.open(this.name, 1);
45
+ request.onupgradeneeded = () => {
46
+ request.result.createObjectStore(ENTRIES);
47
+ request.result.createObjectStore(CUSTODY);
48
+ };
49
+ request.onsuccess = () => resolve(request.result);
50
+ request.onerror = () => reject(request.error ?? new Error(`${this.name} did not open`));
51
+ });
52
+ return this.#open;
53
+ }
54
+ async close() {
55
+ const open = this.#open;
56
+ this.#open = undefined;
57
+ if (open)
58
+ (await open).close();
59
+ }
60
+ }
61
+ export class IdbMemory extends Memory {
62
+ #db;
63
+ constructor(name = BROWSER_HARBOR) {
64
+ super();
65
+ this.#db = new Database(name);
66
+ }
67
+ // The database it keeps in.
68
+ get name() {
69
+ return this.#db.name;
70
+ }
71
+ async read(place) {
72
+ const store = (await this.#db.get()).transaction(ENTRIES).objectStore(ENTRIES);
73
+ const range = IDBKeyRange.bound([place, ''], [place, END]);
74
+ const [keys, values] = await Promise.all([done(store.getAllKeys(range)), done(store.getAll(range))]);
75
+ return new Map(keys.map((key, i) => [key[1], new Uint8Array(values[i])]));
76
+ }
77
+ async write(place, entries) {
78
+ const tx = (await this.#db.get()).transaction(ENTRIES, 'readwrite');
79
+ const kept = finished(tx);
80
+ const store = tx.objectStore(ENTRIES);
81
+ try {
82
+ for (const [name, bytes] of entries)
83
+ this.put(store, [place, name], bytes === null ? null : bytes.slice().buffer);
84
+ }
85
+ catch (e) {
86
+ tx.abort();
87
+ await kept.catch(() => undefined);
88
+ throw e;
89
+ }
90
+ await kept;
91
+ }
92
+ // One entry of a keep: its bytes, or null where it goes.
93
+ put(store, key, bytes) {
94
+ if (bytes === null)
95
+ store.delete(key);
96
+ else
97
+ store.put(bytes, key);
98
+ }
99
+ async places() {
100
+ const keys = await done((await this.#db.get()).transaction(ENTRIES).objectStore(ENTRIES).getAllKeys());
101
+ return [...new Set(keys.map((key) => key[0]))];
102
+ }
103
+ async forget(place) {
104
+ const tx = (await this.#db.get()).transaction(ENTRIES, 'readwrite');
105
+ tx.objectStore(ENTRIES).delete(IDBKeyRange.bound([place, ''], [place, END]));
106
+ await finished(tx);
107
+ }
108
+ close() {
109
+ return this.#db.close();
110
+ }
111
+ }
112
+ export class IdbCustody extends Custody {
113
+ #db;
114
+ #entropy;
115
+ constructor(name = BROWSER_HARBOR, entropy = new CryptoEntropy()) {
116
+ super();
117
+ this.#db = new Database(name);
118
+ this.#entropy = entropy;
119
+ }
120
+ // The seed the database holds, or a drawn one sealed there the first
121
+ // time. Two first asks at once keep the one added first.
122
+ async seed() {
123
+ const db = await this.#db.get();
124
+ const kept = (await done(db.transaction(CUSTODY).objectStore(CUSTODY).get(SEED)));
125
+ if (kept)
126
+ return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: kept.iv }, kept.key, kept.sealed));
127
+ const seed = this.#entropy.draw(32);
128
+ const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
129
+ const iv = new Uint8Array(this.#entropy.draw(12));
130
+ const sealed = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new Uint8Array(seed));
131
+ const tx = db.transaction(CUSTODY, 'readwrite');
132
+ tx.objectStore(CUSTODY).add({ key, iv, sealed }, SEED);
133
+ try {
134
+ await finished(tx);
135
+ }
136
+ catch (e) {
137
+ if (e.name === 'ConstraintError')
138
+ return this.seed();
139
+ throw e;
140
+ }
141
+ return seed;
142
+ }
143
+ close() {
144
+ return this.#db.close();
145
+ }
146
+ }
147
+ // A harbor of the browser: its database, the web opened at genesis, Web
148
+ // Push beside it, and one run to a database, held by a Web Lock where the
149
+ // engine has them.
150
+ export class BrowserTerrain extends PointerTerrain {
151
+ where;
152
+ #release;
153
+ constructor(parts = {}) {
154
+ const where = parts.where ?? BROWSER_HARBOR;
155
+ const entropy = new CryptoEntropy();
156
+ super({ entropy, loader: new HeldLoader(parts.modules ?? []), custody: new IdbCustody(where, entropy), memory: new IdbMemory(where), faculties: [webFaculty(), webPush], opens: [WEB_OPENING] });
157
+ this.where = where;
158
+ }
159
+ async claim() {
160
+ const locks = globalThis.navigator?.locks;
161
+ if (!locks)
162
+ return;
163
+ const held = await new Promise((granted) => {
164
+ void locks.request(`nervur:${this.where}`, { ifAvailable: true }, (lock) => {
165
+ if (lock === null) {
166
+ granted(false);
167
+ return undefined;
168
+ }
169
+ granted(true);
170
+ return new Promise((release) => (this.#release = release));
171
+ });
172
+ });
173
+ if (!held)
174
+ throw new Error(`a harbor is already open on ${this.where}`);
175
+ }
176
+ async release() {
177
+ this.#release?.();
178
+ this.#release = undefined;
179
+ await this.memory.close();
180
+ await this.custody.close();
181
+ }
182
+ }
183
+ // A page or a worker with IndexedDB and Web Crypto.
184
+ export const browserGround = {
185
+ name: BROWSER,
186
+ fits: () => typeof indexedDB !== 'undefined' && typeof crypto?.subtle !== 'undefined',
187
+ terrain: ({ modules, where }) => new BrowserTerrain({ ...(where ? { where } : {}), ...(modules ? { modules } : {}) }),
188
+ };
@@ -0,0 +1,12 @@
1
+ import type { JsonObject } from '../being/index.ts';
2
+ import { type Harbor } from '../harbor/index.ts';
3
+ type Scope = {
4
+ addEventListener(type: string, listener: (event: never) => void): void;
5
+ };
6
+ export declare const ringAll: (harbor: Harbor) => Promise<JsonObject[]>;
7
+ export declare const serveWorker: (harbor: Promise<Harbor>, scope?: Scope) => void;
8
+ export type Reaching = {
9
+ postMessage(message: unknown, transfer: Transferable[]): void;
10
+ };
11
+ export declare const rootOf: (worker: Reaching) => (request: JsonObject) => Promise<JsonObject>;
12
+ export {};
@@ -0,0 +1,40 @@
1
+ import { RELAY_CLIENT, rootLine } from '../harbor/index.js';
2
+ const answer = async (harbor, data, reply) => {
3
+ if (!reply)
4
+ return;
5
+ let out;
6
+ try {
7
+ out = await rootLine(await harbor, data?.request);
8
+ }
9
+ catch {
10
+ out = { error: 'the harbor threw' };
11
+ }
12
+ reply.postMessage(out);
13
+ };
14
+ // Every relay client of the harbor asked to collect what waits.
15
+ export const ringAll = async (harbor) => {
16
+ const keys = Object.entries(harbor.box.partition.beings)
17
+ .filter(([, row]) => row.class === RELAY_CLIENT)
18
+ .map(([key]) => key);
19
+ return Promise.all(keys.map(async (being) => (await harbor.root('ask', { being, method: 'ring' }))));
20
+ };
21
+ // Called as the worker's script starts, so no tab's first message is
22
+ // lost while the harbor opens.
23
+ export const serveWorker = (harbor, scope = globalThis) => {
24
+ scope.addEventListener('connect', (event) => {
25
+ const port = event.ports[0];
26
+ port.onmessage = (message) => void answer(harbor, message.data, message.ports[0]);
27
+ port.start();
28
+ });
29
+ scope.addEventListener('message', (event) => void answer(harbor, event.data, event.ports[0]));
30
+ scope.addEventListener('push', (event) => event.waitUntil(harbor.then(ringAll)));
31
+ };
32
+ // A tab's root line to the worker's harbor.
33
+ export const rootOf = (worker) => (request) => new Promise((resolve) => {
34
+ const channel = new MessageChannel();
35
+ channel.port1.onmessage = (message) => {
36
+ channel.port1.close();
37
+ resolve(message.data);
38
+ };
39
+ worker.postMessage({ request }, [channel.port2]);
40
+ });
@@ -0,0 +1 @@
1
+ export * from './index.ts';
@@ -0,0 +1,7 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur` as a browser bundle reads it: the main entry, with the browser
3
+ // ground added, so `Harbor.open()` keeps a harbor in IndexedDB.
4
+ import { browserGround } from './browser/index.js';
5
+ import { addGround } from './harbor/index.js';
6
+ export * from './index.js';
7
+ addGround(browserGround);
@@ -7,7 +7,7 @@ export declare const parse: (argv: string[]) => {
7
7
  };
8
8
  export declare const requestOf: (command: string, words: string[], flags: Record<string, string>) => RootRequest;
9
9
  export declare const main: (argv: string[], env: Record<string, string | undefined>, print: (line: string) => void) => Promise<number>;
10
- export declare const pilotRequests: (name: string, invitation: JsonObject, at: string) => RootRequest[];
10
+ export declare const pilotRequests: (name: string, invitation: JsonObject, at?: string, web?: boolean) => RootRequest[];
11
11
  export declare const via: (pilot: string, far: RootRequest) => RootRequest;
12
12
  export declare const farAnswer: (local: JsonObject) => JsonObject;
13
13
  export declare const SERVING = -1;