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.
- package/README.md +174 -12
- package/dist/browser/index.d.ts +35 -0
- package/dist/browser/index.js +188 -0
- package/dist/browser/worker.d.ts +12 -0
- package/dist/browser/worker.js +40 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +7 -0
- package/dist/cli/command.d.ts +1 -1
- package/dist/cli/command.js +84 -20
- package/dist/cli/harbor.d.ts +7 -12
- package/dist/cli/harbor.js +73 -123
- package/dist/contract/index.d.ts +2 -1
- package/dist/contract/index.js +3 -7
- package/dist/edge/index.d.ts +71 -0
- package/dist/edge/index.js +334 -0
- package/dist/edge.d.ts +1 -0
- package/dist/edge.js +8 -0
- package/dist/folder/index.d.ts +4 -0
- package/dist/folder/index.js +57 -14
- package/dist/harbor/carrying.d.ts +17 -0
- package/dist/harbor/carrying.js +38 -0
- package/dist/harbor/catalogue.d.ts +2 -0
- package/dist/harbor/catalogue.js +28 -20
- package/dist/harbor/dock.d.ts +22 -3
- package/dist/harbor/dock.js +69 -13
- package/dist/harbor/harbor.d.ts +14 -4
- package/dist/harbor/harbor.js +144 -15
- package/dist/harbor/index.d.ts +6 -3
- package/dist/harbor/index.js +4 -1
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +52 -1
- package/dist/harbor/probe.d.ts +18 -0
- package/dist/harbor/probe.js +15 -0
- package/dist/harbor/registry.d.ts +1 -0
- package/dist/harbor/registry.js +11 -0
- package/dist/harbor/relay.d.ts +15 -0
- package/dist/harbor/relay.js +406 -0
- package/dist/harbor/root-line.js +4 -2
- package/dist/harbor/terrain.d.ts +12 -0
- package/dist/harbor/terrain.js +19 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +156 -0
- package/dist/http/websocket.d.ts +29 -0
- package/dist/http/websocket.js +133 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -2
- package/dist/line/answer.d.ts +8 -0
- package/dist/line/answer.js +60 -0
- package/dist/line/frame.d.ts +34 -0
- package/dist/line/frame.js +81 -0
- package/dist/line/ground.d.ts +3 -0
- package/dist/line/ground.js +9 -0
- package/dist/line/index.d.ts +5 -0
- package/dist/line/index.js +9 -0
- package/dist/line/push.d.ts +7 -0
- package/dist/line/push.js +92 -0
- package/dist/line/web.d.ts +30 -0
- package/dist/line/web.js +181 -0
- package/dist/node/index.d.ts +36 -0
- package/dist/node/index.js +228 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +7 -0
- package/dist/pointer/bodies.d.ts +2 -0
- package/dist/pointer/bodies.js +10 -0
- package/dist/pointer/world.d.ts +7 -1
- package/dist/pointer/world.js +6 -0
- package/dist/quo/address.d.ts +19 -0
- package/dist/quo/address.js +84 -0
- package/dist/quo/door.d.ts +1 -1
- package/dist/quo/door.js +4 -2
- package/dist/quo/index.d.ts +1 -0
- package/dist/quo/index.js +1 -0
- package/dist/quo/invitation.d.ts +1 -0
- package/dist/quo/invitation.js +9 -5
- package/dist/tcp/frame.d.ts +0 -4
- package/dist/tcp/frame.js +0 -10
- package/dist/tcp/index.d.ts +8 -2
- package/dist/tcp/index.js +123 -27
- package/dist/ward/index.d.ts +2 -2
- package/dist/ward/index.js +1 -1
- package/dist/ward/stance.d.ts +2 -0
- package/dist/ward/stance.js +3 -1
- package/dist/ward/ward-being.d.ts +2 -1
- package/dist/ward/ward-being.js +1 -1
- package/dist/ward/ward.d.ts +7 -0
- package/dist/ward/ward.js +6 -0
- package/package.json +21 -2
- package/src/browser/index.ts +207 -0
- package/src/browser/worker.ts +66 -0
- package/src/browser.ts +9 -0
- package/src/cli/command.ts +74 -20
- package/src/cli/harbor.ts +73 -116
- package/src/contract/index.ts +11 -8
- package/src/edge/index.ts +413 -0
- package/src/edge.ts +10 -0
- package/src/folder/index.ts +57 -12
- package/src/harbor/carrying.ts +56 -0
- package/src/harbor/catalogue.ts +32 -17
- package/src/harbor/dock.ts +88 -17
- package/src/harbor/harbor.ts +146 -17
- package/src/harbor/index.ts +6 -3
- package/src/harbor/package.ts +55 -1
- package/src/harbor/probe.ts +34 -0
- package/src/harbor/registry.ts +11 -0
- package/src/harbor/relay.ts +418 -0
- package/src/harbor/root-line.ts +4 -1
- package/src/harbor/terrain.ts +28 -2
- package/src/http/index.ts +150 -0
- package/src/http/websocket.ts +124 -0
- package/src/index.ts +35 -2
- package/src/line/answer.ts +59 -0
- package/src/line/frame.ts +88 -0
- package/src/line/ground.ts +19 -0
- package/src/line/index.ts +9 -0
- package/src/line/push.ts +112 -0
- package/src/line/web.ts +227 -0
- package/src/node/index.ts +234 -0
- package/src/node.ts +9 -0
- package/src/pointer/bodies.ts +11 -0
- package/src/pointer/world.ts +18 -2
- package/src/quo/address.ts +87 -0
- package/src/quo/door.ts +4 -2
- package/src/quo/index.ts +1 -0
- package/src/quo/invitation.ts +10 -6
- package/src/stand/stand.ts +60 -21
- package/src/tcp/index.ts +142 -31
- package/src/ward/index.ts +2 -2
- package/src/ward/stance.ts +6 -1
- package/src/ward/ward-being.ts +2 -2
- package/src/ward/ward.ts +16 -0
- 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
|
|
122
|
-
and harbors in two processes, or a harbor and
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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`.
|
|
148
|
-
|
|
149
|
-
the
|
|
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>'
|
|
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
|
|
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';
|
package/dist/browser.js
ADDED
|
@@ -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);
|
package/dist/cli/command.d.ts
CHANGED
|
@@ -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
|
|
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;
|