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.
Files changed (157) hide show
  1. package/README.md +229 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +5 -1
  4. package/dist/being/faculty.d.ts +1 -0
  5. package/dist/being/faculty.js +13 -7
  6. package/dist/being/index.d.ts +1 -0
  7. package/dist/being/index.js +1 -0
  8. package/dist/being/kind.d.ts +4 -0
  9. package/dist/being/kind.js +28 -0
  10. package/dist/being/types.d.ts +3 -4
  11. package/dist/browser/index.d.ts +35 -0
  12. package/dist/browser/index.js +188 -0
  13. package/dist/browser/worker.d.ts +12 -0
  14. package/dist/browser/worker.js +40 -0
  15. package/dist/browser.d.ts +1 -0
  16. package/dist/browser.js +7 -0
  17. package/dist/cli/command.d.ts +13 -0
  18. package/dist/cli/command.js +237 -0
  19. package/dist/cli/harbor.d.ts +19 -0
  20. package/dist/cli/harbor.js +99 -0
  21. package/dist/cli/main.d.ts +2 -0
  22. package/dist/cli/main.js +14 -0
  23. package/dist/contract/index.d.ts +16 -2
  24. package/dist/contract/index.js +13 -3
  25. package/dist/edge/index.d.ts +57 -0
  26. package/dist/edge/index.js +306 -0
  27. package/dist/edge.d.ts +1 -0
  28. package/dist/edge.js +8 -0
  29. package/dist/folder/index.d.ts +9 -4
  30. package/dist/folder/index.js +89 -84
  31. package/dist/harbor/carrying.d.ts +17 -0
  32. package/dist/harbor/carrying.js +38 -0
  33. package/dist/harbor/catalogue.d.ts +27 -0
  34. package/dist/harbor/catalogue.js +101 -0
  35. package/dist/harbor/dock.d.ts +32 -3
  36. package/dist/harbor/dock.js +98 -12
  37. package/dist/harbor/harbor.d.ts +27 -8
  38. package/dist/harbor/harbor.js +217 -26
  39. package/dist/harbor/index.d.ts +8 -2
  40. package/dist/harbor/index.js +8 -1
  41. package/dist/harbor/package.d.ts +12 -0
  42. package/dist/harbor/package.js +130 -0
  43. package/dist/harbor/probe.d.ts +18 -0
  44. package/dist/harbor/probe.js +15 -0
  45. package/dist/harbor/registry.d.ts +6 -3
  46. package/dist/harbor/registry.js +83 -7
  47. package/dist/harbor/relay.d.ts +15 -0
  48. package/dist/harbor/relay.js +387 -0
  49. package/dist/harbor/root-line.d.ts +9 -0
  50. package/dist/harbor/root-line.js +38 -0
  51. package/dist/harbor/terrain.d.ts +14 -1
  52. package/dist/harbor/terrain.js +19 -0
  53. package/dist/http/index.d.ts +4 -0
  54. package/dist/http/index.js +156 -0
  55. package/dist/http/websocket.d.ts +29 -0
  56. package/dist/http/websocket.js +133 -0
  57. package/dist/index.d.ts +5 -4
  58. package/dist/index.js +12 -6
  59. package/dist/line/answer.d.ts +8 -0
  60. package/dist/line/answer.js +60 -0
  61. package/dist/line/frame.d.ts +34 -0
  62. package/dist/line/frame.js +81 -0
  63. package/dist/line/ground.d.ts +3 -0
  64. package/dist/line/ground.js +9 -0
  65. package/dist/line/index.d.ts +5 -0
  66. package/dist/line/index.js +9 -0
  67. package/dist/line/push.d.ts +7 -0
  68. package/dist/line/push.js +92 -0
  69. package/dist/line/web.d.ts +30 -0
  70. package/dist/line/web.js +181 -0
  71. package/dist/node/index.d.ts +32 -0
  72. package/dist/node/index.js +164 -0
  73. package/dist/node.d.ts +1 -0
  74. package/dist/node.js +7 -0
  75. package/dist/pointer/bodies.d.ts +11 -3
  76. package/dist/pointer/bodies.js +47 -18
  77. package/dist/pointer/index.d.ts +1 -1
  78. package/dist/pointer/index.js +1 -1
  79. package/dist/pointer/world.d.ts +12 -5
  80. package/dist/pointer/world.js +29 -14
  81. package/dist/quo/address.d.ts +19 -0
  82. package/dist/quo/address.js +84 -0
  83. package/dist/quo/door.d.ts +1 -1
  84. package/dist/quo/door.js +6 -4
  85. package/dist/quo/index.d.ts +1 -0
  86. package/dist/quo/index.js +1 -0
  87. package/dist/quo/invitation.d.ts +1 -0
  88. package/dist/quo/invitation.js +9 -5
  89. package/dist/quo/keys.js +1 -1
  90. package/dist/tcp/frame.d.ts +25 -0
  91. package/dist/tcp/frame.js +64 -0
  92. package/dist/tcp/index.d.ts +24 -0
  93. package/dist/tcp/index.js +260 -0
  94. package/dist/ward/index.d.ts +3 -2
  95. package/dist/ward/index.js +2 -1
  96. package/dist/ward/pilot.d.ts +8 -0
  97. package/dist/ward/pilot.js +50 -0
  98. package/dist/ward/stance.d.ts +4 -4
  99. package/dist/ward/stance.js +10 -7
  100. package/dist/ward/ward-being.d.ts +4 -1
  101. package/dist/ward/ward-being.js +8 -2
  102. package/dist/ward/ward.d.ts +11 -2
  103. package/dist/ward/ward.js +41 -5
  104. package/package.json +28 -2
  105. package/src/being/being.ts +5 -1
  106. package/src/being/faculty.ts +14 -7
  107. package/src/being/index.ts +1 -0
  108. package/src/being/kind.ts +30 -0
  109. package/src/being/types.ts +8 -6
  110. package/src/browser/index.ts +207 -0
  111. package/src/browser/worker.ts +66 -0
  112. package/src/browser.ts +9 -0
  113. package/src/cli/command.ts +224 -0
  114. package/src/cli/harbor.ts +103 -0
  115. package/src/cli/main.ts +13 -0
  116. package/src/contract/index.ts +42 -10
  117. package/src/edge/index.ts +359 -0
  118. package/src/edge.ts +10 -0
  119. package/src/folder/index.ts +87 -84
  120. package/src/harbor/carrying.ts +56 -0
  121. package/src/harbor/catalogue.ts +125 -0
  122. package/src/harbor/dock.ts +122 -15
  123. package/src/harbor/harbor.ts +240 -36
  124. package/src/harbor/index.ts +10 -3
  125. package/src/harbor/package.ts +147 -0
  126. package/src/harbor/probe.ts +34 -0
  127. package/src/harbor/registry.ts +73 -9
  128. package/src/harbor/relay.ts +399 -0
  129. package/src/harbor/root-line.ts +39 -0
  130. package/src/harbor/terrain.ts +31 -3
  131. package/src/http/index.ts +150 -0
  132. package/src/http/websocket.ts +124 -0
  133. package/src/index.ts +39 -6
  134. package/src/line/answer.ts +59 -0
  135. package/src/line/frame.ts +88 -0
  136. package/src/line/ground.ts +19 -0
  137. package/src/line/index.ts +9 -0
  138. package/src/line/push.ts +112 -0
  139. package/src/line/web.ts +227 -0
  140. package/src/node/index.ts +170 -0
  141. package/src/node.ts +9 -0
  142. package/src/pointer/bodies.ts +45 -16
  143. package/src/pointer/index.ts +1 -1
  144. package/src/pointer/world.ts +38 -19
  145. package/src/quo/address.ts +87 -0
  146. package/src/quo/door.ts +6 -4
  147. package/src/quo/index.ts +1 -0
  148. package/src/quo/invitation.ts +10 -6
  149. package/src/quo/keys.ts +1 -1
  150. package/src/stand/main.ts +1 -0
  151. package/src/stand/stand.ts +87 -6
  152. package/src/tcp/index.ts +284 -0
  153. package/src/ward/index.ts +3 -2
  154. package/src/ward/pilot.ts +50 -0
  155. package/src/ward/stance.ts +14 -8
  156. package/src/ward/ward-being.ts +9 -3
  157. 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', [SystemTime, Greeter]);
47
- await harbor.root('open', { key: 'clock', class: 'SystemTime' });
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: 'Greeter' });
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
- 1. The terrain gives the box ward's seed and memory.
59
- 2. The box ward unpacks, its ward-being the dock.
60
- 3. Its faculties are born from their rows.
61
- 4. Every ward the dock hosts unpacks.
62
- 5. In every ward, the ward-being first, then each being from her row.
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 classes you hand a harbor only resolve a
65
- name a row keeps. Opening a harbor on empty memory is its genesis, and
66
- the root's asks write the rest: `open`, `host`, and `boot` on a ward.
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 terrain
73
- fulfils. Each has one suite, and every body that fulfils it passes it.
74
- - **Harbor, Terrain, Registry, Dock, WardBeing.** The onion.
75
- - **World, PointerTerrain** and the pointer bodies. A whole world with
76
- this package alone.
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
- Carriers over a network, storage beyond a folder, key custody, and every
80
- screen are `@nervur-org/dock`'s, as classes fulfilling these contracts.
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
 
@@ -5,6 +5,7 @@ export type AskSpec = {
5
5
  readonly for?: (asker: Asker, notes: JsonObject | undefined) => boolean;
6
6
  };
7
7
  export declare class Being {
8
+ static readonly kind: string;
8
9
  static cells: JsonObject;
9
10
  static asks: Record<string, AskSpec>;
10
11
  readonly stance: Stance;
@@ -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 = {};
@@ -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;
@@ -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 { ... } the contract
6
- // class IntervalTimer extends Timer { ... } one class that fulfils it
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. Two asks are the
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 name.
22
+ // `Faculty`, by kind. A class among them with no kind of its own throws.
17
23
  static contracts(C) {
18
- const names = [];
24
+ const kinds = [];
19
25
  for (let c = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c))
20
- names.push(c.name);
21
- return names;
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;
@@ -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';
@@ -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,4 @@
1
+ export declare const KIT = "org.nervur.";
2
+ export declare const isKind: (text: unknown) => text is string;
3
+ export declare const ownKind: (C: unknown) => string | undefined;
4
+ export declare const kindOf: (C: unknown) => string;
@@ -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
+ };
@@ -48,10 +48,8 @@ export interface Stance {
48
48
  readonly cells: JsonObject;
49
49
  readonly occupants: Occupants;
50
50
  readonly standings: Standings;
51
- boot(className: string, key: string, id?: string): Promise<string | null>;
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
+ };