nervur 0.20.0 → 0.20.2
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 +229 -20
- package/dist/being/being.d.ts +1 -0
- package/dist/being/being.js +5 -1
- package/dist/being/faculty.d.ts +1 -0
- package/dist/being/faculty.js +13 -7
- package/dist/being/index.d.ts +1 -0
- package/dist/being/index.js +1 -0
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +3 -4
- 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 +13 -0
- package/dist/cli/command.js +237 -0
- package/dist/cli/harbor.d.ts +19 -0
- package/dist/cli/harbor.js +99 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +16 -2
- package/dist/contract/index.js +13 -3
- package/dist/edge/index.d.ts +57 -0
- package/dist/edge/index.js +306 -0
- package/dist/edge.d.ts +1 -0
- package/dist/edge.js +8 -0
- package/dist/folder/index.d.ts +9 -4
- package/dist/folder/index.js +89 -84
- package/dist/harbor/carrying.d.ts +17 -0
- package/dist/harbor/carrying.js +38 -0
- package/dist/harbor/catalogue.d.ts +27 -0
- package/dist/harbor/catalogue.js +101 -0
- package/dist/harbor/dock.d.ts +32 -3
- package/dist/harbor/dock.js +98 -12
- package/dist/harbor/harbor.d.ts +27 -8
- package/dist/harbor/harbor.js +217 -26
- package/dist/harbor/index.d.ts +8 -2
- package/dist/harbor/index.js +8 -1
- package/dist/harbor/package.d.ts +12 -0
- package/dist/harbor/package.js +130 -0
- package/dist/harbor/probe.d.ts +18 -0
- package/dist/harbor/probe.js +15 -0
- package/dist/harbor/registry.d.ts +6 -3
- package/dist/harbor/registry.js +83 -7
- package/dist/harbor/relay.d.ts +15 -0
- package/dist/harbor/relay.js +387 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +38 -0
- package/dist/harbor/terrain.d.ts +14 -1
- 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 +5 -4
- package/dist/index.js +12 -6
- 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 +32 -0
- package/dist/node/index.js +164 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +7 -0
- package/dist/pointer/bodies.d.ts +11 -3
- package/dist/pointer/bodies.js +47 -18
- package/dist/pointer/index.d.ts +1 -1
- package/dist/pointer/index.js +1 -1
- package/dist/pointer/world.d.ts +12 -5
- package/dist/pointer/world.js +29 -14
- 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 +6 -4
- 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/quo/keys.js +1 -1
- package/dist/tcp/frame.d.ts +25 -0
- package/dist/tcp/frame.js +64 -0
- package/dist/tcp/index.d.ts +24 -0
- package/dist/tcp/index.js +260 -0
- package/dist/ward/index.d.ts +3 -2
- package/dist/ward/index.js +2 -1
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +4 -4
- package/dist/ward/stance.js +10 -7
- package/dist/ward/ward-being.d.ts +4 -1
- package/dist/ward/ward-being.js +8 -2
- package/dist/ward/ward.d.ts +11 -2
- package/dist/ward/ward.js +41 -5
- package/package.json +28 -2
- package/src/being/being.ts +5 -1
- package/src/being/faculty.ts +14 -7
- package/src/being/index.ts +1 -0
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +8 -6
- 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 +224 -0
- package/src/cli/harbor.ts +103 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +42 -10
- package/src/edge/index.ts +359 -0
- package/src/edge.ts +10 -0
- package/src/folder/index.ts +87 -84
- package/src/harbor/carrying.ts +56 -0
- package/src/harbor/catalogue.ts +125 -0
- package/src/harbor/dock.ts +122 -15
- package/src/harbor/harbor.ts +240 -36
- package/src/harbor/index.ts +10 -3
- package/src/harbor/package.ts +147 -0
- package/src/harbor/probe.ts +34 -0
- package/src/harbor/registry.ts +73 -9
- package/src/harbor/relay.ts +399 -0
- package/src/harbor/root-line.ts +39 -0
- package/src/harbor/terrain.ts +31 -3
- package/src/http/index.ts +150 -0
- package/src/http/websocket.ts +124 -0
- package/src/index.ts +39 -6
- 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 +170 -0
- package/src/node.ts +9 -0
- package/src/pointer/bodies.ts +45 -16
- package/src/pointer/index.ts +1 -1
- package/src/pointer/world.ts +38 -19
- package/src/quo/address.ts +87 -0
- package/src/quo/door.ts +6 -4
- package/src/quo/index.ts +1 -0
- package/src/quo/invitation.ts +10 -6
- package/src/quo/keys.ts +1 -1
- package/src/stand/main.ts +1 -0
- package/src/stand/stand.ts +87 -6
- package/src/tcp/index.ts +284 -0
- package/src/ward/index.ts +3 -2
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +14 -8
- package/src/ward/ward-being.ts +9 -3
- package/src/ward/ward.ts +53 -6
package/README.md
CHANGED
|
@@ -18,14 +18,51 @@ 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:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { harborObject } from 'nervur/edge';
|
|
42
|
+
import * as greetings from './greetings.js';
|
|
43
|
+
|
|
44
|
+
export const Harbor = harborObject([greetings], async (harbor) => {
|
|
45
|
+
// the worker's own code is the root: runs once the harbor stands
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Each object keeps its harbor in its storage, its seed sealed under the
|
|
50
|
+
worker's `NERVUR_SECRET`, answers Quo over the web from its requests,
|
|
51
|
+
and asks over TCP and the web. Anywhere else it lives as long as its
|
|
52
|
+
process and speaks the web. Hand in a terrain, as below, and that one
|
|
53
|
+
is used.
|
|
54
|
+
|
|
21
55
|
## A world in one process
|
|
22
56
|
|
|
23
57
|
```js
|
|
24
58
|
import { Being, Faculty, World } from 'nervur';
|
|
25
59
|
|
|
26
|
-
// A contract, and a faculty that fulfils it.
|
|
27
|
-
class Clock extends Faculty {
|
|
60
|
+
// A contract, and a faculty that fulfils it. Every class names its kind.
|
|
61
|
+
class Clock extends Faculty {
|
|
62
|
+
static kind = 'org.example.clock';
|
|
63
|
+
}
|
|
28
64
|
class SystemTime extends Clock {
|
|
65
|
+
static kind = 'org.example.system-time';
|
|
29
66
|
static asks = { now: {} };
|
|
30
67
|
now() {
|
|
31
68
|
return { now: Date.now() };
|
|
@@ -34,6 +71,7 @@ class SystemTime extends Clock {
|
|
|
34
71
|
|
|
35
72
|
// A being, who lends a clock by its contract.
|
|
36
73
|
class Greeter extends Being {
|
|
74
|
+
static kind = 'org.example.greeter';
|
|
37
75
|
static asks = { hello: {} };
|
|
38
76
|
async hello(args, asker) {
|
|
39
77
|
if (!this.stance.standings.get('clock')) await this.stance.lend(Clock, 'clock');
|
|
@@ -42,42 +80,213 @@ class Greeter extends Being {
|
|
|
42
80
|
}
|
|
43
81
|
}
|
|
44
82
|
|
|
83
|
+
// The module these classes are: its name, its version, its classes.
|
|
84
|
+
const greetings = { module: 'org.example.greetings', version: '1.0.0', classes: [SystemTime, Greeter] };
|
|
85
|
+
|
|
86
|
+
// A harbor whose terrain can load that module, and whose catalogue runs it.
|
|
45
87
|
const world = new World();
|
|
46
|
-
const harbor = await world.harbor('home', [
|
|
47
|
-
await harbor.root('
|
|
88
|
+
const harbor = await world.harbor('home', [greetings]);
|
|
89
|
+
await harbor.root('ask', { being: 'catalogue', method: 'add', args: { from: 'org.example.greetings' } });
|
|
90
|
+
await harbor.root('open', { key: 'clock', class: 'org.example.system-time' });
|
|
48
91
|
await harbor.root('host', { ward: 'alice' });
|
|
49
|
-
await harbor.ward('alice').root('boot', { key: 'greeter', class: '
|
|
92
|
+
await harbor.ward('alice').root('boot', { key: 'greeter', class: 'org.example.greeter' });
|
|
50
93
|
```
|
|
51
94
|
|
|
52
95
|
A second harbor in the same world takes an invitation on the greeter and
|
|
53
96
|
asks it through the world, sealed as Quo's bytes. `world.restart('home')`
|
|
54
97
|
opens the harbor again from what its memory kept.
|
|
55
98
|
|
|
99
|
+
## Kinds
|
|
100
|
+
|
|
101
|
+
A row keeps the kind of the class a being is born of, and a being lends
|
|
102
|
+
by a contract's kind. A kind is declared on the class, because a bundler
|
|
103
|
+
renames classes. It is a domain you own, reversed, then a name, as Apple
|
|
104
|
+
and Matrix name things: `com.acme.shop`. Every segment is lowercase
|
|
105
|
+
letters, digits and `-`, and there are three at least. Each class
|
|
106
|
+
declares its own: a subclass that stands, and every contract between a
|
|
107
|
+
faculty and `Faculty`. `org.nervur.` is the kit's, and `org.example.` is
|
|
108
|
+
for examples like the one above. A module's classes stand under its own
|
|
109
|
+
domain: `com.acme.shop` stands `com.acme.*` kinds. A harbor refuses a
|
|
110
|
+
module in which one kind names two classes or two contracts, or names a
|
|
111
|
+
class and a contract.
|
|
112
|
+
|
|
113
|
+
## Modules
|
|
114
|
+
|
|
115
|
+
Your classes come in modules: `{ module, version, classes }`, a module
|
|
116
|
+
named as a kind is. A harbor's catalogue is a faculty of its own, and her
|
|
117
|
+
cells name the modules the harbor runs. `add { from }` runs the module
|
|
118
|
+
the terrain's loader finds there, and `remove { module }` stops one no
|
|
119
|
+
being stands on. A restart loads every module she names and refuses to
|
|
120
|
+
stand when one is missing. A module found at a new version wakes, and
|
|
121
|
+
`modules` shows the version kept and the version running.
|
|
122
|
+
|
|
56
123
|
## The onion
|
|
57
124
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
125
|
+
A harbor is one package. Only the seed and the terrain's bodies, its
|
|
126
|
+
loader among them, stand outside it.
|
|
127
|
+
|
|
128
|
+
1. The terrain gives the harbor's seed, which opens its memory. The
|
|
129
|
+
terrain keeps that memory as sealed bytes and reads none of it.
|
|
130
|
+
2. The box ward unpacks under the seed, its ward-being the dock.
|
|
131
|
+
3. The catalogue wakes, and loads every module she names.
|
|
132
|
+
4. The other faculties are born from their rows.
|
|
133
|
+
5. The routes the dock keeps are handed to the carrier.
|
|
134
|
+
6. Every ward the dock hosts unpacks, under the seed the dock keeps for it.
|
|
135
|
+
7. In every ward, the ward-being first, then each being from her row.
|
|
63
136
|
|
|
64
|
-
Cells decide what stands. The
|
|
65
|
-
|
|
66
|
-
the root's asks write the rest: `
|
|
137
|
+
Cells decide what stands. The modules only resolve a kind a row keeps.
|
|
138
|
+
Opening a harbor on empty memory is its genesis: the dock stands the
|
|
139
|
+
catalogue, and the root's asks write the rest: `add` on the catalogue,
|
|
140
|
+
`open`, `host`, `route`, and `boot` on a ward. `host { ward, seed? }`
|
|
141
|
+
stands a ward under a seed you name, or a drawn one.
|
|
67
142
|
|
|
68
143
|
## What it holds
|
|
69
144
|
|
|
70
145
|
- **Being, Faculty.** What you extend. A faculty's contract is its class
|
|
71
146
|
and every class between it and `Faculty`.
|
|
72
|
-
- **Entropy, Clock, Custody, Memory, Carrier.** The contracts a
|
|
73
|
-
fulfils. Each has one suite, and every body that fulfils it
|
|
74
|
-
|
|
75
|
-
- **
|
|
76
|
-
|
|
147
|
+
- **Entropy, Clock, Custody, Memory, Carrier, Loader.** The contracts a
|
|
148
|
+
terrain fulfils. Each has one suite, and every body that fulfils it
|
|
149
|
+
passes it.
|
|
150
|
+
- **Harbor, Terrain, Dock, Catalogue, Registry, WardBeing.** The onion.
|
|
151
|
+
- **World, PointerTerrain** and the pointer bodies, `HeldLoader` among
|
|
152
|
+
them. A whole world with this package alone.
|
|
153
|
+
- **addGround, grounds**: the grounds `Harbor.open()` tries, and a way to
|
|
154
|
+
add your own.
|
|
155
|
+
- **NodeTerrain**, from `nervur/node`: the Node ground's terrain, for a
|
|
156
|
+
folder you name in code.
|
|
157
|
+
- **BrowserTerrain, IdbMemory, IdbCustody**, from `nervur/browser`: the
|
|
158
|
+
browser ground's terrain and bodies. `serveWorker(Harbor.open())` at
|
|
159
|
+
the top of a shared or service worker holds one harbor for every tab,
|
|
160
|
+
each tab asks it with `rootOf(worker)`, and a push the service worker
|
|
161
|
+
hears makes every relay client `ring`.
|
|
77
162
|
- **FolderMemory, FolderCustody**, from `nervur/folder`.
|
|
163
|
+
- **TcpCarrier, TcpListener**, from `nervur/tcp`: Quo over TCP. Route a
|
|
164
|
+
ward pk to its `tcp://host:port` addresses on the carrier, hand the
|
|
165
|
+
listener your harbor, and harbors in two processes, or a harbor and
|
|
166
|
+
another kit, speak. `tcpFaculty` is the same carrier as a faculty of a
|
|
167
|
+
harbor.
|
|
168
|
+
- **WebDialer, webFaculty**: Quo over the web, a post to `https://` or a
|
|
169
|
+
held line to `wss://`, in any engine with `fetch` and WebSocket.
|
|
170
|
+
`webServe` and `webGround`, from `nervur/http`, listen for it on Node.
|
|
171
|
+
|
|
172
|
+
- **The relay**, `org.nervur.relay` and `org.nervur.relay-client`, in
|
|
173
|
+
every harbor: a harbor nobody can dial is reached through one that
|
|
174
|
+
can, by relations alone. See "A harbor nobody dials" below.
|
|
175
|
+
- **Push, webPush**: a ring with no content, as Web Push sends it,
|
|
176
|
+
signed with VAPID, on fetch and Web Crypto.
|
|
177
|
+
|
|
178
|
+
Carriers such as MCP, and every screen, are `@nervur-org/*`'s, as
|
|
179
|
+
classes fulfilling these contracts.
|
|
180
|
+
|
|
181
|
+
## The command
|
|
182
|
+
|
|
183
|
+
`nervur` stands a harbor on this machine: its seed and sealed memory in a
|
|
184
|
+
folder, Quo over TCP as its carrier. A module is a file that exports
|
|
185
|
+
`module`, `version` and `classes`.
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
nervur init --dir ./harbor
|
|
189
|
+
nervur module add ./greetings.js --dir ./harbor
|
|
190
|
+
nervur serve --dir ./harbor --port 7000
|
|
191
|
+
nervur reach tcp://harbor.example:7000 --dir ./harbor
|
|
192
|
+
nervur open web org.nervur.web --dir ./harbor
|
|
193
|
+
nervur ask web listen '{"port":8080,"path":"/quo"}' --dir ./harbor
|
|
194
|
+
nervur host alice --dir ./harbor
|
|
195
|
+
nervur boot --ward alice greeter org.example.greeter --dir ./harbor
|
|
196
|
+
nervur ask --ward alice greeter hello --dir ./harbor
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`reach` names where the harbor is reached, and every invitation it gives
|
|
200
|
+
carries those addresses in `at`. With no `reach`, an invitation carries
|
|
201
|
+
where the harbor listens, which serves a harbor callers reach directly;
|
|
202
|
+
behind a router or a proxy, name the public address with `reach`. A
|
|
203
|
+
harbor that takes such an invitation keeps them as that ward's route,
|
|
204
|
+
so nobody routes it by hand. Where an invitation carries no `at`,
|
|
205
|
+
`nervur route <ward pk> tcp://host:port` names the route, and a route
|
|
206
|
+
named this way is trusted first.
|
|
207
|
+
|
|
208
|
+
`init` makes the harbor and prints its pk and an owner invitation. `serve`
|
|
209
|
+
listens for Quo on TCP and for the root's asks on a local socket, open to
|
|
210
|
+
you alone. Every other command is one root ask, sent to the served
|
|
211
|
+
harbor, or answered from the folder when none is served. Each prints one
|
|
212
|
+
JSON line. `--dir` defaults to `$NERVUR_DIR`, then `~/.nervur`. The
|
|
213
|
+
catalogue keeps a module by its file's absolute path, and routes live in
|
|
214
|
+
the harbor like everything else, so a restart finds both. `nervur
|
|
215
|
+
modules` shows what runs, and `nervur module remove <module>` stops one.
|
|
216
|
+
|
|
217
|
+
A harbor elsewhere is piloted with the owner invitation its `init`
|
|
218
|
+
printed, from a harbor of your own:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
nervur pilot far '<owner invitation>' --dir ./mine
|
|
222
|
+
nervur host shop --via far --dir ./mine
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`pilot` boots an `org.nervur.pilot` being in your harbor that holds the
|
|
226
|
+
invitation as an ordinary Quo relation, kept in your harbor's memory, and
|
|
227
|
+
reaches the far harbor where its invitation's `at` says; where it says
|
|
228
|
+
nothing, name the address after the invitation. With
|
|
229
|
+
`--via far`, any command is asked of the far harbor as its owner, sealed
|
|
230
|
+
over TCP, except `own` and `disown`, which stay the far root's.
|
|
231
|
+
`nervur unboot far` lets it go.
|
|
232
|
+
|
|
233
|
+
`rootLine(harbor, request)` is the same request in code, for any other
|
|
234
|
+
front: `{ ward?, method?, args? }` in, one JSON answer out.
|
|
235
|
+
|
|
236
|
+
## A harbor nobody dials
|
|
237
|
+
|
|
238
|
+
A device behind a home router listens nowhere a caller can reach. A
|
|
239
|
+
served harbor that can be reached relays for it. On the relay's harbor:
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
nervur open relay org.nervur.relay --dir ./relay
|
|
243
|
+
nervur ask relay invite '{"id":"house","wards":["<camera ward pk>"]}' --dir ./relay
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
On the device, with the relay's address as its own reach:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
nervur open client org.nervur.relay-client --dir ./house
|
|
250
|
+
nervur ask client hold '{"invitation":<the invitation>,"line":true}' --dir ./house
|
|
251
|
+
nervur reach tcp://relay.example:7000 --dir ./house
|
|
252
|
+
nervur serve --dir ./house
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Every invitation the device gives now sends callers to the relay. The
|
|
256
|
+
relay holds each sealed box, the client collects it by an ordinary ask,
|
|
257
|
+
hands it to the device's door, and returns the sealed reply in its next
|
|
258
|
+
ask. Nothing is opened on the way. `line` keeps collecting, for a
|
|
259
|
+
camera. Without it the client is a doorbell: `ring` collects what waits,
|
|
260
|
+
and `"every": 60000` rings on a schedule, for a garage door.
|
|
261
|
+
|
|
262
|
+
A doorbell can also be rung by a push with nothing in it. The relay's
|
|
263
|
+
harbor opens Web Push and names who it is:
|
|
264
|
+
|
|
265
|
+
```sh
|
|
266
|
+
nervur open push org.nervur.web-push --dir ./relay
|
|
267
|
+
nervur ask push subject '{"subject":"mailto:ops@example.org"}' --dir ./relay
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The device asks its client for the key (`nervur ask client key`),
|
|
271
|
+
subscribes to pushes with it, and hands the subscription's endpoint to
|
|
272
|
+
`nervur ask client bell '{"endpoint":"https://..."}'`. When a box waits
|
|
273
|
+
and no line collects, the relay rings that endpoint once. Whatever hears
|
|
274
|
+
the push on the device asks the client's `ring`.
|
|
275
|
+
|
|
276
|
+
## Moving a harbor
|
|
277
|
+
|
|
278
|
+
A harbor moves to another machine by its bytes, with no harbor open:
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
nervur export --with-seed --dir ./harbor > harbor.json
|
|
282
|
+
nervur import harbor.json --dir ./elsewhere
|
|
283
|
+
```
|
|
78
284
|
|
|
79
|
-
|
|
80
|
-
|
|
285
|
+
`export` reads the folder's sealed places, and carries the seed only
|
|
286
|
+
where you ask for it. `import` writes them into a folder that holds none.
|
|
287
|
+
A package with no seed lands where custody already holds the one it was
|
|
288
|
+
sealed under. In code, `copyPackage(from, to)` carries every place of one
|
|
289
|
+
memory into another.
|
|
81
290
|
|
|
82
291
|
## License
|
|
83
292
|
|
package/dist/being/being.d.ts
CHANGED
package/dist/being/being.js
CHANGED
|
@@ -9,8 +9,12 @@ const wrote = (self, name) => {
|
|
|
9
9
|
return false;
|
|
10
10
|
};
|
|
11
11
|
// A subclass's statics replace its parent's and never merge: her blueprint
|
|
12
|
-
// is what the class in front of you declares, in its order.
|
|
12
|
+
// is what the class in front of you declares, in its order. Her kind is
|
|
13
|
+
// never inherited: see `kind.ts`.
|
|
13
14
|
export class Being {
|
|
15
|
+
// The name her row keeps for this class. Every class that stands declares
|
|
16
|
+
// its own.
|
|
17
|
+
static kind = 'org.nervur.being';
|
|
14
18
|
// Her cells' defaults, written at birth only where a key is missing.
|
|
15
19
|
static cells = {};
|
|
16
20
|
static asks = {};
|
package/dist/being/faculty.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Being } from './being.ts';
|
|
|
2
2
|
import { type Asker, type Blueprint, type JsonObject, type Reply } from './types.ts';
|
|
3
3
|
export declare class Faculty extends Being {
|
|
4
4
|
#private;
|
|
5
|
+
static readonly kind: string;
|
|
5
6
|
static contracts(C: abstract new (...args: never[]) => unknown): string[];
|
|
6
7
|
static fulfils(C: unknown): C is typeof Faculty;
|
|
7
8
|
describe(asker: Asker): Blueprint;
|
package/dist/being/faculty.js
CHANGED
|
@@ -2,23 +2,29 @@
|
|
|
2
2
|
// A faculty: a being of the box ward that fulfils a contract. A contract is
|
|
3
3
|
// a class between `Faculty` and the class that stands, usually abstract:
|
|
4
4
|
//
|
|
5
|
-
// abstract class Timer extends Faculty {
|
|
6
|
-
//
|
|
5
|
+
// abstract class Timer extends Faculty { the contract
|
|
6
|
+
// static override kind = 'com.acme.timer' }
|
|
7
|
+
// class IntervalTimer extends Timer { one class that fulfils it
|
|
8
|
+
// static override kind = 'com.acme.interval-timer' }
|
|
7
9
|
//
|
|
8
|
-
// A being lends by the contract and never learns the class.
|
|
10
|
+
// A being lends by the contract's kind and never learns the class. Every
|
|
11
|
+
// class between the faculty and `Faculty` is a contract and declares its
|
|
12
|
+
// own kind, and a faculty fulfils each of them and her own. Two asks are the
|
|
9
13
|
// faculty's own and answered to the ward-being that opened her alone:
|
|
10
14
|
// `offer`, an invitation on herself for a ward the dock vouches for, and
|
|
11
15
|
// `retract`, an offer that was not taken.
|
|
12
16
|
import { Being } from './being.js';
|
|
17
|
+
import { kindOf } from './kind.js';
|
|
13
18
|
import { WARD } from './types.js';
|
|
14
19
|
export class Faculty extends Being {
|
|
20
|
+
static kind = 'org.nervur.faculty';
|
|
15
21
|
// The contracts a class fulfils: itself and every class between it and
|
|
16
|
-
// `Faculty`, by
|
|
22
|
+
// `Faculty`, by kind. A class among them with no kind of its own throws.
|
|
17
23
|
static contracts(C) {
|
|
18
|
-
const
|
|
24
|
+
const kinds = [];
|
|
19
25
|
for (let c = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c))
|
|
20
|
-
|
|
21
|
-
return
|
|
26
|
+
kinds.push(kindOf(c));
|
|
27
|
+
return kinds;
|
|
22
28
|
}
|
|
23
29
|
static fulfils(C) {
|
|
24
30
|
return typeof C === 'function' && C.prototype instanceof Faculty;
|
package/dist/being/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Being, type AskSpec } from './being.ts';
|
|
2
2
|
export { canonical, digest } from './digest.ts';
|
|
3
3
|
export { Faculty } from './faculty.ts';
|
|
4
|
+
export { isKind, KIT, kindOf, ownKind } from './kind.ts';
|
|
4
5
|
export { OWNER, WARD, type Answer, type Ask, type Asker, type BeingClass, type BeingLike, type Blueprint, type Invitation, type Json, type JsonObject, type Occupants, type Reply, type Schema, type Stance, type Standing, type Standings, type Wanted } from './types.ts';
|
|
5
6
|
export { isSilence, isWord, silence, told, word, type Silence, type Word, type WordName } from './words.ts';
|
package/dist/being/index.js
CHANGED
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
export { Being } from './being.js';
|
|
4
4
|
export { canonical, digest } from './digest.js';
|
|
5
5
|
export { Faculty } from './faculty.js';
|
|
6
|
+
export { isKind, KIT, kindOf, ownKind } from './kind.js';
|
|
6
7
|
export { OWNER, WARD } from './types.js';
|
|
7
8
|
export { isSilence, isWord, silence, told, word } from './words.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A kind: the name a row keeps for the class a being is born of, and the
|
|
3
|
+
// name a contract is lent by. It is declared on the class, never taken from
|
|
4
|
+
// the name the language gives it, because a bundler renames classes.
|
|
5
|
+
//
|
|
6
|
+
// static kind = 'com.acme.shop'
|
|
7
|
+
//
|
|
8
|
+
// A kind is a reversed domain its author owns, then a name: at least three
|
|
9
|
+
// segments joined by `.`, each lowercase letters, digits and `-`, starting
|
|
10
|
+
// with a letter or digit. `org.nervur.` is the kit's own. A class declares
|
|
11
|
+
// its own kind: one it inherits names its parent, so it is none.
|
|
12
|
+
export const KIT = 'org.nervur.';
|
|
13
|
+
const KIND = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*){2,}$/;
|
|
14
|
+
export const isKind = (text) => typeof text === 'string' && KIND.test(text);
|
|
15
|
+
// The kind a class declares on itself, or undefined.
|
|
16
|
+
export const ownKind = (C) => {
|
|
17
|
+
if (typeof C !== 'function' || !Object.hasOwn(C, 'kind'))
|
|
18
|
+
return undefined;
|
|
19
|
+
const kind = C.kind;
|
|
20
|
+
return isKind(kind) ? kind : undefined;
|
|
21
|
+
};
|
|
22
|
+
// The kind a class declares on itself, or a throw naming the class.
|
|
23
|
+
export const kindOf = (C) => {
|
|
24
|
+
const kind = ownKind(C);
|
|
25
|
+
if (kind === undefined)
|
|
26
|
+
throw new Error(`class ${typeof C === 'function' ? C.name : String(C)} declares no kind of its own`);
|
|
27
|
+
return kind;
|
|
28
|
+
};
|
package/dist/being/types.d.ts
CHANGED
|
@@ -48,10 +48,8 @@ export interface Stance {
|
|
|
48
48
|
readonly cells: JsonObject;
|
|
49
49
|
readonly occupants: Occupants;
|
|
50
50
|
readonly standings: Standings;
|
|
51
|
-
boot(
|
|
52
|
-
lend(contract:
|
|
53
|
-
readonly name: string;
|
|
54
|
-
}, id: string): Promise<string | null>;
|
|
51
|
+
boot(kind: string, key: string, id?: string): Promise<string | null>;
|
|
52
|
+
lend(contract: abstract new (...args: never[]) => unknown, id: string): Promise<string | null>;
|
|
55
53
|
}
|
|
56
54
|
export interface BeingLike {
|
|
57
55
|
answer(asker: Asker, method: string | undefined, args: JsonObject): Reply | Promise<Reply>;
|
|
@@ -59,4 +57,5 @@ export interface BeingLike {
|
|
|
59
57
|
}
|
|
60
58
|
export type BeingClass = (new (stance: Stance) => BeingLike) & {
|
|
61
59
|
readonly name: string;
|
|
60
|
+
readonly kind: string;
|
|
62
61
|
};
|
|
@@ -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
|
+
};
|