nervur 0.20.1 → 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 +130 -8
- 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 +59 -16
- package/dist/cli/harbor.d.ts +9 -11
- package/dist/cli/harbor.js +81 -123
- package/dist/contract/index.d.ts +2 -1
- package/dist/contract/index.js +3 -7
- 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 +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 +2 -0
- package/dist/harbor/package.js +15 -0
- 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 +387 -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 +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 +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 +53 -17
- package/src/cli/harbor.ts +82 -116
- package/src/contract/index.ts +11 -8
- package/src/edge/index.ts +359 -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 +16 -0
- package/src/harbor/probe.ts +34 -0
- package/src/harbor/registry.ts +11 -0
- package/src/harbor/relay.ts +399 -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 +170 -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/dist/tcp/frame.js
CHANGED
|
@@ -62,13 +62,3 @@ export class FrameReader {
|
|
|
62
62
|
return out;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
// "host:port" to its parts, or null. A host in brackets is an IPv6 address.
|
|
66
|
-
export const parseAt = (at) => {
|
|
67
|
-
if (typeof at !== 'string')
|
|
68
|
-
return null;
|
|
69
|
-
const m = /^(?:\[([^\]]+)\]|([^:[\]]+)):(\d{1,5})$/.exec(at);
|
|
70
|
-
if (!m)
|
|
71
|
-
return null;
|
|
72
|
-
const port = Number(m[3]);
|
|
73
|
-
return port > 0 && port < 65_536 ? { host: m[1] ?? m[2], port } : null;
|
|
74
|
-
};
|
package/dist/tcp/index.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Carrier } from '../contract/index.ts';
|
|
2
|
-
|
|
2
|
+
import { type Dialed, type GroundFaculty } from '../harbor/index.ts';
|
|
3
|
+
import { TCP, TCP_OPENING } from '../line/index.ts';
|
|
3
4
|
export interface Doors {
|
|
4
5
|
arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
5
6
|
}
|
|
6
7
|
export declare class TcpCarrier extends Carrier {
|
|
7
8
|
#private;
|
|
8
|
-
route(pk: string, at: string | null): boolean;
|
|
9
|
+
route(pk: string, at: readonly string[] | null): boolean;
|
|
10
|
+
routed(pk: string): boolean;
|
|
9
11
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
12
|
+
carryAt(at: readonly string[], pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
13
|
+
dial(at: string, pk: string, bytes: Uint8Array): Promise<Dialed>;
|
|
10
14
|
close(): Promise<void>;
|
|
11
15
|
}
|
|
12
16
|
export declare class TcpListener {
|
|
@@ -16,3 +20,5 @@ export declare class TcpListener {
|
|
|
16
20
|
get at(): string;
|
|
17
21
|
close(): Promise<void>;
|
|
18
22
|
}
|
|
23
|
+
export { TCP, TCP_OPENING };
|
|
24
|
+
export declare const tcpFaculty: GroundFaculty;
|
package/dist/tcp/index.js
CHANGED
|
@@ -3,20 +3,26 @@
|
|
|
3
3
|
// sockets.
|
|
4
4
|
//
|
|
5
5
|
// TcpCarrier a Carrier that dials: bytes to a ward pk go in an ask
|
|
6
|
-
// frame to the
|
|
6
|
+
// frame to the `tcp://host:port` addresses its book holds
|
|
7
|
+
// for that pk, in their order
|
|
7
8
|
// TcpListener answers ask frames for the doors it is handed, a harbor
|
|
8
|
-
// among them
|
|
9
|
+
// among them, at a `tcp://` address
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// A connection is opened per address, carries many asks at once, and is
|
|
12
|
+
// dropped when either side closes it; the next ask dials again. An ask
|
|
13
|
+
// goes to the next address only when the one before could not be dialed,
|
|
14
|
+
// since an ask a line took may have been heard.
|
|
13
15
|
import { connect, createServer } from 'node:net';
|
|
14
16
|
import { hex, isHex, unhex } from '../crypto/index.js';
|
|
15
17
|
import { Carrier } from '../contract/index.js';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
+
import { CarrierFaculty, UNDIALED } from '../harbor/index.js';
|
|
19
|
+
import { tcpAddress, tcpAt } from '../quo/index.js';
|
|
20
|
+
import { ownerAsk } from '../ward/index.js';
|
|
21
|
+
import { askFrame, FrameReader, nothingFrame, replyFrame, TCP, TCP_OPENING } from '../line/index.js';
|
|
18
22
|
const MAX_ID = 0xffff_ffff;
|
|
19
|
-
|
|
23
|
+
// The `tcp` addresses among some, in their order.
|
|
24
|
+
const tcpAddresses = (at) => at.map(tcpAddress).filter((a) => a !== null);
|
|
25
|
+
class Line {
|
|
20
26
|
#socket;
|
|
21
27
|
#reader = new FrameReader();
|
|
22
28
|
#waiting = new Map();
|
|
@@ -47,7 +53,7 @@ class Dialed {
|
|
|
47
53
|
static open(host, port, onClose) {
|
|
48
54
|
return new Promise((resolve) => {
|
|
49
55
|
const socket = connect({ host, port });
|
|
50
|
-
socket.once('connect', () => resolve(new
|
|
56
|
+
socket.once('connect', () => resolve(new Line(socket, onClose)));
|
|
51
57
|
socket.once('error', () => {
|
|
52
58
|
socket.destroy();
|
|
53
59
|
resolve(null);
|
|
@@ -71,31 +77,55 @@ class Dialed {
|
|
|
71
77
|
export class TcpCarrier extends Carrier {
|
|
72
78
|
#routes = new Map();
|
|
73
79
|
#lines = new Map();
|
|
74
|
-
//
|
|
75
|
-
//
|
|
80
|
+
// Where a ward pk is reached, replacing any route before: its `tcp`
|
|
81
|
+
// addresses in order, the others skipped. A list with none is not
|
|
82
|
+
// taken; null forgets the route.
|
|
76
83
|
route(pk, at) {
|
|
77
|
-
if (!isHex(pk, 64)
|
|
84
|
+
if (!isHex(pk, 64))
|
|
78
85
|
return false;
|
|
79
86
|
if (at === null)
|
|
80
|
-
this.#routes.delete(pk);
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
return this.#routes.delete(pk) || true;
|
|
88
|
+
const mine = tcpAddresses(at);
|
|
89
|
+
if (mine.length === 0)
|
|
90
|
+
return false;
|
|
91
|
+
this.#routes.set(pk, mine);
|
|
83
92
|
return true;
|
|
84
93
|
}
|
|
85
|
-
//
|
|
94
|
+
// Whether a ward pk has a route.
|
|
95
|
+
routed(pk) {
|
|
96
|
+
return this.#routes.has(pk);
|
|
97
|
+
}
|
|
98
|
+
// Nothing where the pk has no route, no address is reached, or the
|
|
86
99
|
// connection closed before an answer.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
#
|
|
100
|
+
carry(pk, bytes) {
|
|
101
|
+
return this.#carry(this.#routes.get(pk) ?? [], pk, bytes);
|
|
102
|
+
}
|
|
103
|
+
// Bytes to a ward pk at the addresses named, as a route would carry them.
|
|
104
|
+
carryAt(at, pk, bytes) {
|
|
105
|
+
return isHex(pk, 64) ? this.#carry(tcpAddresses(at), pk, bytes) : Promise.resolve(null);
|
|
106
|
+
}
|
|
107
|
+
async #carry(addresses, pk, bytes) {
|
|
108
|
+
for (const address of addresses) {
|
|
109
|
+
const out = await this.#dial(address, pk, bytes);
|
|
110
|
+
if (out !== UNDIALED)
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// Bytes to a ward pk at one address, or `UNDIALED` where no line opens.
|
|
116
|
+
dial(at, pk, bytes) {
|
|
117
|
+
const address = tcpAddress(at);
|
|
118
|
+
return address && isHex(pk, 64) ? this.#dial(address, pk, bytes) : Promise.resolve(UNDIALED);
|
|
119
|
+
}
|
|
120
|
+
async #dial(address, pk, bytes) {
|
|
121
|
+
const line = await this.#line(address);
|
|
122
|
+
return line ? line.ask(unhex(pk), bytes) : UNDIALED;
|
|
123
|
+
}
|
|
124
|
+
#line({ host, port }) {
|
|
125
|
+
const at = tcpAt(host, port);
|
|
95
126
|
let line = this.#lines.get(at);
|
|
96
127
|
if (!line) {
|
|
97
|
-
|
|
98
|
-
line = Dialed.open(host, port, () => this.#lines.delete(at)).then((dialed) => {
|
|
128
|
+
line = Line.open(host, port, () => this.#lines.delete(at)).then((dialed) => {
|
|
99
129
|
if (!dialed)
|
|
100
130
|
this.#lines.delete(at);
|
|
101
131
|
return dialed;
|
|
@@ -127,9 +157,10 @@ export class TcpListener {
|
|
|
127
157
|
listener.#server.listen(port, host, () => resolve(listener));
|
|
128
158
|
});
|
|
129
159
|
}
|
|
160
|
+
// The `tcp://host:port` address this listener answers at.
|
|
130
161
|
get at() {
|
|
131
162
|
const address = this.#server.address();
|
|
132
|
-
return address.
|
|
163
|
+
return tcpAt(address.address, address.port);
|
|
133
164
|
}
|
|
134
165
|
#serve(socket) {
|
|
135
166
|
this.#sockets.add(socket);
|
|
@@ -162,3 +193,68 @@ export class TcpListener {
|
|
|
162
193
|
return new Promise((resolve) => this.#server.close(() => resolve()));
|
|
163
194
|
}
|
|
164
195
|
}
|
|
196
|
+
export { TCP, TCP_OPENING };
|
|
197
|
+
const listenOf = (value) => {
|
|
198
|
+
if (typeof value !== 'object' || value === null)
|
|
199
|
+
return null;
|
|
200
|
+
const { host, port } = value;
|
|
201
|
+
return typeof host === 'string' && Number.isInteger(port) && port >= 0 && port <= 65_535 ? { host, port: port } : null;
|
|
202
|
+
};
|
|
203
|
+
// Quo over TCP as a carrier faculty of one harbor. It dials every `tcp://`
|
|
204
|
+
// address the harbor routes, and listens where its cells say:
|
|
205
|
+
//
|
|
206
|
+
// listen { host?, port? } listen there from now on, 127.0.0.1 and a
|
|
207
|
+
// free port unless named, and on every wake
|
|
208
|
+
// at the port it bound
|
|
209
|
+
// deaf listen nowhere from now on
|
|
210
|
+
export const tcpFaculty = (ground) => class TcpFaculty extends CarrierFaculty {
|
|
211
|
+
static kind = TCP;
|
|
212
|
+
static asks = {
|
|
213
|
+
listen: ownerAsk('listen for Quo over TCP, and on every wake', { properties: { host: { type: 'string' }, port: { type: 'integer' } } }),
|
|
214
|
+
deaf: ownerAsk('listen nowhere'),
|
|
215
|
+
};
|
|
216
|
+
#dialer = new TcpCarrier();
|
|
217
|
+
#listener;
|
|
218
|
+
accepts(address) {
|
|
219
|
+
return tcpAddress(address) !== null;
|
|
220
|
+
}
|
|
221
|
+
dial(address, pk, bytes) {
|
|
222
|
+
return this.#dialer.dial(address, pk, bytes);
|
|
223
|
+
}
|
|
224
|
+
listening() {
|
|
225
|
+
return this.#listener ? [this.#listener.at] : [];
|
|
226
|
+
}
|
|
227
|
+
async awake() {
|
|
228
|
+
const at = listenOf(this.cells.listen);
|
|
229
|
+
if (at && !this.#listener)
|
|
230
|
+
this.#listener = await TcpListener.listen(ground, at.host, at.port);
|
|
231
|
+
}
|
|
232
|
+
async sleep() {
|
|
233
|
+
await this.#dialer.close();
|
|
234
|
+
await this.#listener?.close();
|
|
235
|
+
this.#listener = undefined;
|
|
236
|
+
}
|
|
237
|
+
async listen(args) {
|
|
238
|
+
const at = listenOf({ host: args.host ?? '127.0.0.1', port: args.port ?? 0 });
|
|
239
|
+
if (at === null)
|
|
240
|
+
return { error: 'host is text and port a port' };
|
|
241
|
+
await this.#listener?.close();
|
|
242
|
+
this.#listener = undefined;
|
|
243
|
+
try {
|
|
244
|
+
this.#listener = await TcpListener.listen(ground, at.host, at.port);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
return { error: e.message };
|
|
248
|
+
}
|
|
249
|
+
// The port it bound, so a wake listens where every route and every
|
|
250
|
+
// invitation already points.
|
|
251
|
+
this.cells.listen = { host: at.host, port: tcpAddress(this.#listener.at).port };
|
|
252
|
+
return { at: this.#listener.at };
|
|
253
|
+
}
|
|
254
|
+
async deaf() {
|
|
255
|
+
await this.#listener?.close();
|
|
256
|
+
this.#listener = undefined;
|
|
257
|
+
delete this.cells.listen;
|
|
258
|
+
return { deaf: true };
|
|
259
|
+
}
|
|
260
|
+
};
|
package/dist/ward/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { allow, CEILING, DEFAULT } from './allowance.ts';
|
|
2
2
|
export { HEAD_ROW, Partition, type BeingRow, type Head, type StandingRow } from './partition.ts';
|
|
3
3
|
export { DEPTH } from './stance.ts';
|
|
4
|
-
export { DOCK, Ward, type Catalogue, type Lender, type WardParts } from './ward.ts';
|
|
5
|
-
export { ownerAsk, pilots, WardBeing, type Steward, type WardBeingStance } from './ward-being.ts';
|
|
4
|
+
export { DOCK, Ward, type Catalogue, type Lender, type Routes, type WardParts } from './ward.ts';
|
|
5
|
+
export { ownerAsk, PILOT, pilots, rootAsk, WardBeing, type Steward, type WardBeingStance } from './ward-being.ts';
|
|
6
6
|
export { Pilot } from './pilot.ts';
|
package/dist/ward/index.js
CHANGED
|
@@ -4,5 +4,5 @@ export { allow, CEILING, DEFAULT } from './allowance.js';
|
|
|
4
4
|
export { HEAD_ROW, Partition } from './partition.js';
|
|
5
5
|
export { DEPTH } from './stance.js';
|
|
6
6
|
export { DOCK, Ward } from './ward.js';
|
|
7
|
-
export { ownerAsk, pilots, WardBeing } from './ward-being.js';
|
|
7
|
+
export { ownerAsk, PILOT, pilots, rootAsk, WardBeing } from './ward-being.js';
|
|
8
8
|
export { Pilot } from './pilot.js';
|
package/dist/ward/stance.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface Inside {
|
|
|
14
14
|
boot(maker: string, className: string, key: string, id?: string): Promise<string | null>;
|
|
15
15
|
offer(contract: string): Promise<Invitation | null>;
|
|
16
16
|
retract(contract: string, heir: string): Promise<void>;
|
|
17
|
+
reach(): readonly string[];
|
|
18
|
+
learn(ward: string, at: readonly string[]): Promise<void>;
|
|
17
19
|
}
|
|
18
20
|
export declare class WardStance implements Stance {
|
|
19
21
|
#private;
|
package/dist/ward/stance.js
CHANGED
|
@@ -87,7 +87,7 @@ export class WardStance {
|
|
|
87
87
|
const kept = writeValue(notes, DEPTH);
|
|
88
88
|
if (!this.#free(id, this.#row.occupants) || kept === undefined)
|
|
89
89
|
return null;
|
|
90
|
-
const { heir, invitation } = await this.#inside.door.invite();
|
|
90
|
+
const { heir, invitation } = await this.#inside.door.invite(this.#inside.reach());
|
|
91
91
|
if (!this.#free(id, this.#row.occupants)) {
|
|
92
92
|
this.#inside.door.remove(heir);
|
|
93
93
|
return null;
|
|
@@ -113,6 +113,8 @@ export class WardStance {
|
|
|
113
113
|
const invitation = readInvitation(value);
|
|
114
114
|
if (!invitation || !this.#free(id, this.#row.standings))
|
|
115
115
|
return null;
|
|
116
|
+
if (invitation.at)
|
|
117
|
+
await this.#inside.learn(invitation.ward, invitation.at);
|
|
116
118
|
const wire = new Wire(invitation, freshStanding(), this.#inside.draw);
|
|
117
119
|
const read = await this.#lanes.run(`take:${id}`, async () => {
|
|
118
120
|
// A knock that brought nothing back may have bound: the standing asks
|
|
@@ -18,6 +18,7 @@ export type WardBeingStance = Stance & {
|
|
|
18
18
|
};
|
|
19
19
|
export declare const pilots: (asker: Asker, notes: JsonObject | undefined) => boolean;
|
|
20
20
|
export declare const ownerAsk: (description: string, input?: JsonObject) => AskSpec;
|
|
21
|
+
export declare const rootAsk: (description: string, input?: JsonObject) => AskSpec;
|
|
21
22
|
export declare const PILOT = "org.nervur.pilot";
|
|
22
23
|
export declare class WardBeing extends Being {
|
|
23
24
|
static readonly kind: string;
|
|
@@ -28,7 +29,7 @@ export declare class WardBeing extends Being {
|
|
|
28
29
|
own(args: JsonObject): Promise<JsonObject>;
|
|
29
30
|
disown(args: JsonObject): JsonObject;
|
|
30
31
|
boot(args: JsonObject): Promise<JsonObject>;
|
|
31
|
-
unboot(args: JsonObject): JsonObject
|
|
32
|
+
unboot(args: JsonObject): JsonObject | Promise<JsonObject>;
|
|
32
33
|
public(args: JsonObject): JsonObject;
|
|
33
34
|
invite(args: JsonObject): Promise<JsonObject>;
|
|
34
35
|
ask(args: JsonObject): Promise<JsonObject>;
|
package/dist/ward/ward-being.js
CHANGED
|
@@ -9,7 +9,7 @@ export const pilots = (asker, notes) => asker.id === OWNER || notes?.owner === t
|
|
|
9
9
|
export const ownerAsk = (description, input = {}) => ({ description, input: { type: 'object', ...input }, for: pilots });
|
|
10
10
|
// An ask the root alone may make, so a carried key pilots but never hands
|
|
11
11
|
// piloting on.
|
|
12
|
-
const rootAsk = (description, input = {}) => ({ description, input: { type: 'object', ...input }, for: (asker) => asker.id === OWNER });
|
|
12
|
+
export const rootAsk = (description, input = {}) => ({ description, input: { type: 'object', ...input }, for: (asker) => asker.id === OWNER });
|
|
13
13
|
// The one kit kind the root boots: every other kit class stands where the
|
|
14
14
|
// kit puts it.
|
|
15
15
|
export const PILOT = 'org.nervur.pilot';
|
package/dist/ward/ward.d.ts
CHANGED
|
@@ -16,7 +16,12 @@ export type WardParts = {
|
|
|
16
16
|
readonly carrier: Carrier;
|
|
17
17
|
readonly root?: (stance: WardBeingStance) => WardBeing;
|
|
18
18
|
readonly dock?: Invitation;
|
|
19
|
+
readonly routes?: Routes;
|
|
19
20
|
};
|
|
21
|
+
export interface Routes {
|
|
22
|
+
reach(): readonly string[];
|
|
23
|
+
learn(ward: string, at: readonly string[]): Promise<void>;
|
|
24
|
+
}
|
|
20
25
|
export declare const DOCK = "dock";
|
|
21
26
|
export interface Lender {
|
|
22
27
|
offer(contract: string): Promise<Invitation | null>;
|
|
@@ -41,6 +46,8 @@ export declare class Ward implements Behind, Inside {
|
|
|
41
46
|
send(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
42
47
|
offer(contract: string): Promise<Invitation | null>;
|
|
43
48
|
retract(contract: string, heir: string): Promise<void>;
|
|
49
|
+
reach(): readonly string[];
|
|
50
|
+
learn(ward: string, at: readonly string[]): Promise<void>;
|
|
44
51
|
live(key: string, row: BeingRow): boolean;
|
|
45
52
|
boot(maker: string, className: string, key: string, id?: string): Promise<string | null>;
|
|
46
53
|
}
|
package/dist/ward/ward.js
CHANGED
|
@@ -177,6 +177,12 @@ export class Ward {
|
|
|
177
177
|
return own.being.withdraw(contract, heir);
|
|
178
178
|
await own.stance.standings.get(DOCK)?.ask('retract', { contract, heir });
|
|
179
179
|
}
|
|
180
|
+
reach() {
|
|
181
|
+
return this.#parts.routes?.reach() ?? [];
|
|
182
|
+
}
|
|
183
|
+
learn(ward, at) {
|
|
184
|
+
return this.#parts.routes?.learn(ward, at) ?? Promise.resolve();
|
|
185
|
+
}
|
|
180
186
|
live(key, row) {
|
|
181
187
|
return this.#residents.get(key)?.row === row;
|
|
182
188
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"quo",
|
|
@@ -18,8 +18,23 @@
|
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
|
+
"workerd": "./dist/edge.js",
|
|
22
|
+
"browser": "./dist/browser.js",
|
|
23
|
+
"node": "./dist/node.js",
|
|
21
24
|
"default": "./dist/index.js"
|
|
22
25
|
},
|
|
26
|
+
"./edge": {
|
|
27
|
+
"types": "./dist/edge/index.d.ts",
|
|
28
|
+
"default": "./dist/edge/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./browser": {
|
|
31
|
+
"types": "./dist/browser/index.d.ts",
|
|
32
|
+
"default": "./dist/browser/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./node": {
|
|
35
|
+
"types": "./dist/node/index.d.ts",
|
|
36
|
+
"default": "./dist/node/index.js"
|
|
37
|
+
},
|
|
23
38
|
"./folder": {
|
|
24
39
|
"types": "./dist/folder/index.d.ts",
|
|
25
40
|
"default": "./dist/folder/index.js"
|
|
@@ -27,13 +42,17 @@
|
|
|
27
42
|
"./tcp": {
|
|
28
43
|
"types": "./dist/tcp/index.d.ts",
|
|
29
44
|
"default": "./dist/tcp/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./http": {
|
|
47
|
+
"types": "./dist/http/index.d.ts",
|
|
48
|
+
"default": "./dist/http/index.js"
|
|
30
49
|
}
|
|
31
50
|
},
|
|
32
51
|
"scripts": {
|
|
33
52
|
"build": "tsc -p tsconfig.build.json",
|
|
34
53
|
"check": "tsc -p tsconfig.json && npm run build && node ../../test.mjs \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
35
54
|
"deep": "node ../../test.mjs \"test/terrain/*.test.ts\"",
|
|
36
|
-
"cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-exclude=\"**/src/stand/**\" --test-coverage-exclude=\"../../quo/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
55
|
+
"cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-exclude=\"**/src/stand/**\" --test-coverage-exclude=\"**/src/browser/**\" --test-coverage-exclude=\"**/src/edge/**\" --test-coverage-exclude=\"../../quo/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
|
|
37
56
|
"prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
|
|
38
57
|
},
|
|
39
58
|
"engines": {
|
|
@@ -0,0 +1,207 @@
|
|
|
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, type Entropy, type Module } from '../contract/index.ts';
|
|
14
|
+
import type { GroundProbe } from '../harbor/index.ts';
|
|
15
|
+
import { webFaculty, WEB_OPENING, webPush } from '../line/index.ts';
|
|
16
|
+
import { CryptoEntropy, HeldLoader, PointerTerrain } from '../pointer/index.ts';
|
|
17
|
+
|
|
18
|
+
export { ringAll, rootOf, serveWorker, type Reaching } from './worker.ts';
|
|
19
|
+
|
|
20
|
+
export const BROWSER = 'browser';
|
|
21
|
+
// The database a harbor keeps itself in unless named.
|
|
22
|
+
export const BROWSER_HARBOR = 'nervur';
|
|
23
|
+
|
|
24
|
+
const ENTRIES = 'entries';
|
|
25
|
+
const CUSTODY = 'custody';
|
|
26
|
+
const SEED = 'seed';
|
|
27
|
+
// Past every lowercase hex name, so [place, END] bounds a place's keys.
|
|
28
|
+
const END = '';
|
|
29
|
+
|
|
30
|
+
type Sealed = { key: CryptoKey; iv: Uint8Array<ArrayBuffer>; sealed: ArrayBuffer };
|
|
31
|
+
|
|
32
|
+
const done = <T>(request: IDBRequest<T>): Promise<T> =>
|
|
33
|
+
new Promise((resolve, reject) => {
|
|
34
|
+
request.onsuccess = () => resolve(request.result);
|
|
35
|
+
request.onerror = () => reject(request.error ?? new Error('IndexedDB refused'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// A transaction's end. A request that fails aborts it, and the abort
|
|
39
|
+
// carries that request's error.
|
|
40
|
+
const finished = (tx: IDBTransaction): Promise<void> =>
|
|
41
|
+
new Promise((resolve, reject) => {
|
|
42
|
+
tx.oncomplete = () => resolve();
|
|
43
|
+
tx.onabort = () => reject(tx.error ?? new Error('the keep was aborted'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// One database, opened once per body and made on first open.
|
|
47
|
+
class Database {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
#open: Promise<IDBDatabase> | undefined;
|
|
50
|
+
constructor(name: string) {
|
|
51
|
+
this.name = name;
|
|
52
|
+
}
|
|
53
|
+
get(): Promise<IDBDatabase> {
|
|
54
|
+
this.#open ??= new Promise((resolve, reject) => {
|
|
55
|
+
const request = indexedDB.open(this.name, 1);
|
|
56
|
+
request.onupgradeneeded = () => {
|
|
57
|
+
request.result.createObjectStore(ENTRIES);
|
|
58
|
+
request.result.createObjectStore(CUSTODY);
|
|
59
|
+
};
|
|
60
|
+
request.onsuccess = () => resolve(request.result);
|
|
61
|
+
request.onerror = () => reject(request.error ?? new Error(`${this.name} did not open`));
|
|
62
|
+
});
|
|
63
|
+
return this.#open;
|
|
64
|
+
}
|
|
65
|
+
async close(): Promise<void> {
|
|
66
|
+
const open = this.#open;
|
|
67
|
+
this.#open = undefined;
|
|
68
|
+
if (open) (await open).close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class IdbMemory extends Memory {
|
|
73
|
+
readonly #db: Database;
|
|
74
|
+
constructor(name: string = BROWSER_HARBOR) {
|
|
75
|
+
super();
|
|
76
|
+
this.#db = new Database(name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The database it keeps in.
|
|
80
|
+
get name(): string {
|
|
81
|
+
return this.#db.name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async read(place: string): Promise<Map<string, Uint8Array>> {
|
|
85
|
+
const store = (await this.#db.get()).transaction(ENTRIES).objectStore(ENTRIES);
|
|
86
|
+
const range = IDBKeyRange.bound([place, ''], [place, END]);
|
|
87
|
+
const [keys, values] = await Promise.all([done(store.getAllKeys(range)), done(store.getAll(range))]);
|
|
88
|
+
return new Map(keys.map((key, i) => [(key as [string, string])[1], new Uint8Array(values[i] as ArrayBuffer)]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void> {
|
|
92
|
+
const tx = (await this.#db.get()).transaction(ENTRIES, 'readwrite');
|
|
93
|
+
const kept = finished(tx);
|
|
94
|
+
const store = tx.objectStore(ENTRIES);
|
|
95
|
+
try {
|
|
96
|
+
for (const [name, bytes] of entries) this.put(store, [place, name], bytes === null ? null : bytes.slice().buffer);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
tx.abort();
|
|
99
|
+
await kept.catch(() => undefined);
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
await kept;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// One entry of a keep: its bytes, or null where it goes.
|
|
106
|
+
protected put(store: IDBObjectStore, key: [string, string], bytes: ArrayBuffer | null): void {
|
|
107
|
+
if (bytes === null) store.delete(key);
|
|
108
|
+
else store.put(bytes, key);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async places(): Promise<string[]> {
|
|
112
|
+
const keys = await done((await this.#db.get()).transaction(ENTRIES).objectStore(ENTRIES).getAllKeys());
|
|
113
|
+
return [...new Set(keys.map((key) => (key as [string, string])[0]))];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async forget(place: string): Promise<void> {
|
|
117
|
+
const tx = (await this.#db.get()).transaction(ENTRIES, 'readwrite');
|
|
118
|
+
tx.objectStore(ENTRIES).delete(IDBKeyRange.bound([place, ''], [place, END]));
|
|
119
|
+
await finished(tx);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
close(): Promise<void> {
|
|
123
|
+
return this.#db.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class IdbCustody extends Custody {
|
|
128
|
+
readonly #db: Database;
|
|
129
|
+
readonly #entropy: Entropy;
|
|
130
|
+
constructor(name: string = BROWSER_HARBOR, entropy: Entropy = new CryptoEntropy()) {
|
|
131
|
+
super();
|
|
132
|
+
this.#db = new Database(name);
|
|
133
|
+
this.#entropy = entropy;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The seed the database holds, or a drawn one sealed there the first
|
|
137
|
+
// time. Two first asks at once keep the one added first.
|
|
138
|
+
async seed(): Promise<Uint8Array> {
|
|
139
|
+
const db = await this.#db.get();
|
|
140
|
+
const kept = (await done(db.transaction(CUSTODY).objectStore(CUSTODY).get(SEED))) as Sealed | undefined;
|
|
141
|
+
if (kept) return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: kept.iv }, kept.key, kept.sealed));
|
|
142
|
+
const seed = this.#entropy.draw(32);
|
|
143
|
+
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
144
|
+
const iv = new Uint8Array(this.#entropy.draw(12));
|
|
145
|
+
const sealed = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new Uint8Array(seed));
|
|
146
|
+
const tx = db.transaction(CUSTODY, 'readwrite');
|
|
147
|
+
tx.objectStore(CUSTODY).add({ key, iv, sealed } satisfies Sealed, SEED);
|
|
148
|
+
try {
|
|
149
|
+
await finished(tx);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
if ((e as { name?: string }).name === 'ConstraintError') return this.seed();
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
return seed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
close(): Promise<void> {
|
|
158
|
+
return this.#db.close();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type BrowserParts = { readonly where?: string; readonly modules?: readonly Module[] };
|
|
163
|
+
|
|
164
|
+
// A harbor of the browser: its database, the web opened at genesis, Web
|
|
165
|
+
// Push beside it, and one run to a database, held by a Web Lock where the
|
|
166
|
+
// engine has them.
|
|
167
|
+
export class BrowserTerrain extends PointerTerrain {
|
|
168
|
+
readonly where: string;
|
|
169
|
+
#release: (() => void) | undefined;
|
|
170
|
+
|
|
171
|
+
constructor(parts: BrowserParts = {}) {
|
|
172
|
+
const where = parts.where ?? BROWSER_HARBOR;
|
|
173
|
+
const entropy = new CryptoEntropy();
|
|
174
|
+
super({ entropy, loader: new HeldLoader(parts.modules ?? []), custody: new IdbCustody(where, entropy), memory: new IdbMemory(where), faculties: [webFaculty(), webPush], opens: [WEB_OPENING] });
|
|
175
|
+
this.where = where;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override async claim(): Promise<void> {
|
|
179
|
+
const locks = (globalThis as { navigator?: { locks?: LockManager } }).navigator?.locks;
|
|
180
|
+
if (!locks) return;
|
|
181
|
+
const held = await new Promise<boolean>((granted) => {
|
|
182
|
+
void locks.request(`nervur:${this.where}`, { ifAvailable: true }, (lock) => {
|
|
183
|
+
if (lock === null) {
|
|
184
|
+
granted(false);
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
granted(true);
|
|
188
|
+
return new Promise<void>((release) => (this.#release = release));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
if (!held) throw new Error(`a harbor is already open on ${this.where}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override async release(): Promise<void> {
|
|
195
|
+
this.#release?.();
|
|
196
|
+
this.#release = undefined;
|
|
197
|
+
await (this.memory as IdbMemory).close();
|
|
198
|
+
await (this.custody as IdbCustody).close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A page or a worker with IndexedDB and Web Crypto.
|
|
203
|
+
export const browserGround: GroundProbe = {
|
|
204
|
+
name: BROWSER,
|
|
205
|
+
fits: () => typeof indexedDB !== 'undefined' && typeof crypto?.subtle !== 'undefined',
|
|
206
|
+
terrain: ({ modules, where }) => new BrowserTerrain({ ...(where ? { where } : {}), ...(modules ? { modules } : {}) }),
|
|
207
|
+
};
|