nervur 0.20.0 → 0.20.1
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 +107 -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/cli/command.d.ts +13 -0
- package/dist/cli/command.js +194 -0
- package/dist/cli/harbor.d.ts +21 -0
- package/dist/cli/harbor.js +141 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +15 -2
- package/dist/contract/index.js +16 -2
- package/dist/folder/index.d.ts +5 -4
- package/dist/folder/index.js +45 -83
- package/dist/harbor/catalogue.d.ts +25 -0
- package/dist/harbor/catalogue.js +93 -0
- package/dist/harbor/dock.d.ts +12 -2
- package/dist/harbor/dock.js +39 -9
- package/dist/harbor/harbor.d.ts +15 -6
- package/dist/harbor/harbor.js +83 -21
- package/dist/harbor/index.d.ts +4 -1
- package/dist/harbor/index.js +5 -1
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +115 -0
- package/dist/harbor/registry.d.ts +5 -3
- package/dist/harbor/registry.js +72 -7
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +36 -0
- package/dist/harbor/terrain.d.ts +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/pointer/bodies.d.ts +9 -3
- package/dist/pointer/bodies.js +37 -18
- package/dist/pointer/index.d.ts +1 -1
- package/dist/pointer/index.js +1 -1
- package/dist/pointer/world.d.ts +5 -4
- package/dist/pointer/world.js +23 -14
- package/dist/quo/door.js +2 -2
- package/dist/quo/keys.js +1 -1
- package/dist/tcp/frame.d.ts +29 -0
- package/dist/tcp/frame.js +74 -0
- package/dist/tcp/index.d.ts +18 -0
- package/dist/tcp/index.js +164 -0
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +1 -0
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +2 -4
- package/dist/ward/stance.js +7 -6
- package/dist/ward/ward-being.d.ts +2 -0
- package/dist/ward/ward-being.js +7 -1
- package/dist/ward/ward.d.ts +4 -2
- package/dist/ward/ward.js +35 -5
- package/package.json +8 -1
- 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/cli/command.ts +188 -0
- package/src/cli/harbor.ts +137 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +38 -9
- package/src/folder/index.ts +44 -86
- package/src/harbor/catalogue.ts +110 -0
- package/src/harbor/dock.ts +48 -12
- package/src/harbor/harbor.ts +104 -29
- package/src/harbor/index.ts +6 -2
- package/src/harbor/package.ts +131 -0
- package/src/harbor/registry.ts +62 -9
- package/src/harbor/root-line.ts +36 -0
- package/src/harbor/terrain.ts +3 -1
- package/src/index.ts +5 -5
- package/src/pointer/bodies.ts +34 -16
- package/src/pointer/index.ts +1 -1
- package/src/pointer/world.ts +21 -18
- package/src/quo/door.ts +2 -2
- package/src/quo/keys.ts +1 -1
- package/src/stand/main.ts +1 -0
- package/src/stand/stand.ts +46 -4
- package/src/tcp/frame.ts +78 -0
- package/src/tcp/index.ts +173 -0
- package/src/ward/index.ts +1 -0
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +8 -7
- package/src/ward/ward-being.ts +7 -1
- package/src/ward/ward.ts +37 -6
package/dist/pointer/world.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { Carrier, type Clock, type Custody, type Entropy, type Memory } from '../contract/index.ts';
|
|
1
|
+
import { Carrier, type Clock, type Custody, type Entropy, type Loader, type Memory, type Module } from '../contract/index.ts';
|
|
3
2
|
import { Harbor, Terrain } from '../harbor/index.ts';
|
|
4
3
|
export declare class NoCarrier extends Carrier {
|
|
5
4
|
carry(): Promise<Uint8Array | null>;
|
|
@@ -15,6 +14,7 @@ export type PointerParts = {
|
|
|
15
14
|
custody?: Custody;
|
|
16
15
|
memory?: Memory;
|
|
17
16
|
carrier?: Carrier;
|
|
17
|
+
loader?: Loader;
|
|
18
18
|
};
|
|
19
19
|
export declare class PointerTerrain extends Terrain {
|
|
20
20
|
readonly entropy: Entropy;
|
|
@@ -22,13 +22,14 @@ export declare class PointerTerrain extends Terrain {
|
|
|
22
22
|
readonly custody: Custody;
|
|
23
23
|
readonly memory: Memory;
|
|
24
24
|
readonly carrier: Carrier;
|
|
25
|
+
readonly loader: Loader;
|
|
25
26
|
constructor(parts?: PointerParts);
|
|
26
27
|
}
|
|
27
28
|
export declare class World {
|
|
28
29
|
#private;
|
|
29
30
|
readonly cut: Set<string>;
|
|
30
|
-
harbor(name: string,
|
|
31
|
+
harbor(name: string, modules?: readonly Module[], parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor>;
|
|
31
32
|
get(name: string): Harbor;
|
|
32
|
-
restart(name: string,
|
|
33
|
+
restart(name: string, parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor>;
|
|
33
34
|
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
34
35
|
}
|
package/dist/pointer/world.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The pointer world: harbors in one process, an address book from ward pk
|
|
3
|
+
// to harbor that stands in for the internet, and weather between them. A
|
|
4
|
+
// restart drops a harbor and opens a new one on the terrain the world
|
|
5
|
+
// keeps for it, so only what its memory kept comes back.
|
|
1
6
|
import { Carrier } from '../contract/index.js';
|
|
2
7
|
import { Harbor, Terrain } from '../harbor/index.js';
|
|
3
|
-
import { CryptoEntropy, HeldCustody, SystemClock, VolatileMemory } from './bodies.js';
|
|
8
|
+
import { CryptoEntropy, HeldCustody, HeldLoader, SystemClock, VolatileMemory } from './bodies.js';
|
|
4
9
|
// A carrier that reaches no one: a harbor alone.
|
|
5
10
|
export class NoCarrier extends Carrier {
|
|
6
11
|
carry() {
|
|
@@ -24,6 +29,7 @@ export class PointerTerrain extends Terrain {
|
|
|
24
29
|
custody;
|
|
25
30
|
memory;
|
|
26
31
|
carrier;
|
|
32
|
+
loader;
|
|
27
33
|
constructor(parts = {}) {
|
|
28
34
|
super();
|
|
29
35
|
this.entropy = parts.entropy ?? new CryptoEntropy();
|
|
@@ -31,20 +37,23 @@ export class PointerTerrain extends Terrain {
|
|
|
31
37
|
this.custody = parts.custody ?? new HeldCustody(this.entropy);
|
|
32
38
|
this.memory = parts.memory ?? new VolatileMemory();
|
|
33
39
|
this.carrier = parts.carrier ?? new NoCarrier();
|
|
40
|
+
this.loader = parts.loader ?? new HeldLoader();
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
43
|
export class World {
|
|
37
44
|
#places = new Map();
|
|
38
45
|
// Harbors the world reaches no one in, nor anyone out of.
|
|
39
46
|
cut = new Set();
|
|
40
|
-
// A harbor of this world
|
|
41
|
-
//
|
|
42
|
-
|
|
47
|
+
// A harbor of this world, whose loader holds `modules` unless `parts`
|
|
48
|
+
// names a loader. `parts` replaces the pointer bodies, a folder's memory
|
|
49
|
+
// and custody among them. The harbor runs only the modules its catalogue
|
|
50
|
+
// was told to add.
|
|
51
|
+
async harbor(name, modules = [], parts = {}) {
|
|
43
52
|
if (this.#places.has(name))
|
|
44
53
|
throw new Error(`a harbor named ${name} stands`);
|
|
45
|
-
const terrain = new PointerTerrain({ ...parts, carrier: new PointerCarrier(this) });
|
|
46
|
-
const harbor = await Harbor.open(terrain
|
|
47
|
-
this.#places.set(name, { harbor, terrain
|
|
54
|
+
const terrain = new PointerTerrain({ loader: new HeldLoader(modules), ...parts, carrier: new PointerCarrier(this) });
|
|
55
|
+
const harbor = await Harbor.open(terrain);
|
|
56
|
+
this.#places.set(name, { harbor, terrain });
|
|
48
57
|
return harbor;
|
|
49
58
|
}
|
|
50
59
|
get(name) {
|
|
@@ -53,17 +62,17 @@ export class World {
|
|
|
53
62
|
throw new Error(`no harbor named ${name}`);
|
|
54
63
|
return place.harbor;
|
|
55
64
|
}
|
|
56
|
-
// The harbor opened again on its own terrain
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
async restart(name,
|
|
65
|
+
// The harbor opened again on its own terrain. `parts` replaces bodies of
|
|
66
|
+
// the terrain, as a new process would build them over the same disk, or
|
|
67
|
+
// with other code.
|
|
68
|
+
async restart(name, parts) {
|
|
60
69
|
const place = this.#places.get(name);
|
|
61
70
|
if (!place)
|
|
62
71
|
throw new Error(`no harbor named ${name}`);
|
|
63
72
|
const t = place.terrain;
|
|
64
|
-
const terrain = parts ? new PointerTerrain({ entropy: t.entropy, clock: t.clock, custody: t.custody, memory: t.memory, ...parts, carrier: t.carrier }) : t;
|
|
65
|
-
const harbor = await Harbor.open(terrain
|
|
66
|
-
this.#places.set(name, {
|
|
73
|
+
const terrain = parts ? new PointerTerrain({ entropy: t.entropy, clock: t.clock, custody: t.custody, memory: t.memory, loader: t.loader, ...parts, carrier: t.carrier }) : t;
|
|
74
|
+
const harbor = await Harbor.open(terrain);
|
|
75
|
+
this.#places.set(name, { harbor, terrain });
|
|
67
76
|
return harbor;
|
|
68
77
|
}
|
|
69
78
|
carry(pk, bytes) {
|
package/dist/quo/door.js
CHANGED
|
@@ -23,7 +23,7 @@ export class Door {
|
|
|
23
23
|
this.draw = draw;
|
|
24
24
|
}
|
|
25
25
|
// A new heir, held fresh, and its invitation. The lock is drawn at the
|
|
26
|
-
// first invitation that names
|
|
26
|
+
// first invitation that names an heir, before the heir's secret.
|
|
27
27
|
async invite() {
|
|
28
28
|
let lock = this.relations.lock();
|
|
29
29
|
if (!lock)
|
|
@@ -32,7 +32,7 @@ export class Door {
|
|
|
32
32
|
this.relations.set(heir.id, { state: 'fresh' });
|
|
33
33
|
return { heir: heir.id, invitation: { ward: this.key.pk, heir: heir.id, secret: hex(heir.secret), lock: hex(lock.ek) } };
|
|
34
34
|
}
|
|
35
|
-
// The door stops holding
|
|
35
|
+
// The door stops holding an heir. A fresh one leaves nothing, a spent one
|
|
36
36
|
// leaves the keys kept at removal.
|
|
37
37
|
remove(heir) {
|
|
38
38
|
const record = this.relations.get(heir);
|
package/dist/quo/keys.js
CHANGED
|
@@ -10,7 +10,7 @@ export const LABEL = {
|
|
|
10
10
|
wardSign: utf8('quo-ward-sign'),
|
|
11
11
|
wardSeal: utf8('quo-ward-seal'),
|
|
12
12
|
};
|
|
13
|
-
// An Ed25519 pair:
|
|
13
|
+
// An Ed25519 pair: an heir, or a standing's own key.
|
|
14
14
|
export class SigningKey {
|
|
15
15
|
secret;
|
|
16
16
|
pk;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const ASK = 0;
|
|
2
|
+
export declare const REPLY = 1;
|
|
3
|
+
export declare const NOTHING = 2;
|
|
4
|
+
export declare const MAX_BODY = 1048645;
|
|
5
|
+
export type Frame = {
|
|
6
|
+
kind: 'ask';
|
|
7
|
+
id: number;
|
|
8
|
+
pk: Uint8Array;
|
|
9
|
+
box: Uint8Array;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'reply';
|
|
12
|
+
id: number;
|
|
13
|
+
box: Uint8Array;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'nothing';
|
|
16
|
+
id: number;
|
|
17
|
+
};
|
|
18
|
+
export declare const askFrame: (id: number, pk: Uint8Array, box: Uint8Array) => Uint8Array;
|
|
19
|
+
export declare const replyFrame: (id: number, box: Uint8Array) => Uint8Array;
|
|
20
|
+
export declare const nothingFrame: (id: number) => Uint8Array;
|
|
21
|
+
export declare class FrameReader {
|
|
22
|
+
#private;
|
|
23
|
+
get bad(): boolean;
|
|
24
|
+
push(chunk: Uint8Array): Frame[];
|
|
25
|
+
}
|
|
26
|
+
export declare const parseAt: (at: unknown) => {
|
|
27
|
+
host: string;
|
|
28
|
+
port: number;
|
|
29
|
+
} | null;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The frames of Quo over TCP, `quo/CARRIER-TCP.md`:
|
|
3
|
+
//
|
|
4
|
+
// frame = length (4, big-endian) || kind (1) || id (4, big-endian) || rest
|
|
5
|
+
// 00 ask rest = ward pk (64) || box
|
|
6
|
+
// 01 reply rest = box
|
|
7
|
+
// 02 nothing rest = empty
|
|
8
|
+
//
|
|
9
|
+
// A reader says a stream is not a frame as soon as its bytes say so, and
|
|
10
|
+
// reads nothing after it.
|
|
11
|
+
import { concat } from '../crypto/index.js';
|
|
12
|
+
export const ASK = 0;
|
|
13
|
+
export const REPLY = 1;
|
|
14
|
+
export const NOTHING = 2;
|
|
15
|
+
export const MAX_BODY = 1_048_645;
|
|
16
|
+
const PK = 64;
|
|
17
|
+
const frame = (kind, id, ...rest) => {
|
|
18
|
+
const head = new Uint8Array(9);
|
|
19
|
+
const view = new DataView(head.buffer);
|
|
20
|
+
view.setUint32(0, 5 + rest.reduce((n, r) => n + r.length, 0));
|
|
21
|
+
head[4] = kind;
|
|
22
|
+
view.setUint32(5, id >>> 0);
|
|
23
|
+
return concat(head, ...rest);
|
|
24
|
+
};
|
|
25
|
+
export const askFrame = (id, pk, box) => frame(ASK, id, pk, box);
|
|
26
|
+
export const replyFrame = (id, box) => frame(REPLY, id, box);
|
|
27
|
+
export const nothingFrame = (id) => frame(NOTHING, id);
|
|
28
|
+
// Whether a body of this length, and this first byte when known, is no frame.
|
|
29
|
+
const notAFrame = (length, kind) => length > MAX_BODY || length < 5 || (kind !== undefined && (kind > NOTHING || (kind === ASK && length < 5 + PK) || (kind === NOTHING && length !== 5)));
|
|
30
|
+
export class FrameReader {
|
|
31
|
+
#buffer = new Uint8Array(0);
|
|
32
|
+
#bad = false;
|
|
33
|
+
// True once bytes that are not a frame arrived; nothing is read after.
|
|
34
|
+
get bad() {
|
|
35
|
+
return this.#bad;
|
|
36
|
+
}
|
|
37
|
+
push(chunk) {
|
|
38
|
+
if (this.#bad)
|
|
39
|
+
return [];
|
|
40
|
+
this.#buffer = this.#buffer.length === 0 ? chunk.slice() : concat(this.#buffer, chunk);
|
|
41
|
+
const out = [];
|
|
42
|
+
while (this.#buffer.length >= 4) {
|
|
43
|
+
const view = new DataView(this.#buffer.buffer, this.#buffer.byteOffset);
|
|
44
|
+
const length = view.getUint32(0);
|
|
45
|
+
if (notAFrame(length, this.#buffer.length > 4 ? this.#buffer[4] : undefined)) {
|
|
46
|
+
this.#bad = true;
|
|
47
|
+
this.#buffer = new Uint8Array(0);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
if (this.#buffer.length < 4 + length)
|
|
51
|
+
break;
|
|
52
|
+
const body = this.#buffer.subarray(4, 4 + length);
|
|
53
|
+
this.#buffer = this.#buffer.slice(4 + length);
|
|
54
|
+
const id = new DataView(body.buffer, body.byteOffset).getUint32(1);
|
|
55
|
+
if (body[0] === ASK)
|
|
56
|
+
out.push({ kind: 'ask', id, pk: body.slice(5, 5 + PK), box: body.slice(5 + PK) });
|
|
57
|
+
else if (body[0] === REPLY)
|
|
58
|
+
out.push({ kind: 'reply', id, box: body.slice(5) });
|
|
59
|
+
else
|
|
60
|
+
out.push({ kind: 'nothing', id });
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
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
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Carrier } from '../contract/index.ts';
|
|
2
|
+
export { parseAt } from './frame.ts';
|
|
3
|
+
export interface Doors {
|
|
4
|
+
arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
5
|
+
}
|
|
6
|
+
export declare class TcpCarrier extends Carrier {
|
|
7
|
+
#private;
|
|
8
|
+
route(pk: string, at: string | null): boolean;
|
|
9
|
+
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare class TcpListener {
|
|
13
|
+
#private;
|
|
14
|
+
private constructor();
|
|
15
|
+
static listen(doors: Doors, host?: string, port?: number): Promise<TcpListener>;
|
|
16
|
+
get at(): string;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Quo over TCP: `nervur/tcp`, an entry that names a platform, Node's
|
|
3
|
+
// sockets.
|
|
4
|
+
//
|
|
5
|
+
// TcpCarrier a Carrier that dials: bytes to a ward pk go in an ask
|
|
6
|
+
// frame to the address its book holds for that pk
|
|
7
|
+
// TcpListener answers ask frames for the doors it is handed, a harbor
|
|
8
|
+
// among them
|
|
9
|
+
//
|
|
10
|
+
// Where a ward is reached is the carrier's own, and never on the wire. A
|
|
11
|
+
// connection is opened per address, carries many asks at once, and is
|
|
12
|
+
// dropped when either side closes it; the next ask dials again.
|
|
13
|
+
import { connect, createServer } from 'node:net';
|
|
14
|
+
import { hex, isHex, unhex } from '../crypto/index.js';
|
|
15
|
+
import { Carrier } from '../contract/index.js';
|
|
16
|
+
import { askFrame, FrameReader, nothingFrame, parseAt, replyFrame } from './frame.js';
|
|
17
|
+
export { parseAt } from './frame.js';
|
|
18
|
+
const MAX_ID = 0xffff_ffff;
|
|
19
|
+
class Dialed {
|
|
20
|
+
#socket;
|
|
21
|
+
#reader = new FrameReader();
|
|
22
|
+
#waiting = new Map();
|
|
23
|
+
#next = 1;
|
|
24
|
+
closed = false;
|
|
25
|
+
constructor(socket, onClose) {
|
|
26
|
+
this.#socket = socket;
|
|
27
|
+
socket.on('data', (chunk) => {
|
|
28
|
+
for (const f of this.#reader.push(new Uint8Array(chunk))) {
|
|
29
|
+
if (f.kind === 'ask')
|
|
30
|
+
continue;
|
|
31
|
+
const resolve = this.#waiting.get(f.id);
|
|
32
|
+
this.#waiting.delete(f.id);
|
|
33
|
+
resolve?.(f.kind === 'reply' ? f.box : null);
|
|
34
|
+
}
|
|
35
|
+
if (this.#reader.bad)
|
|
36
|
+
socket.destroy();
|
|
37
|
+
});
|
|
38
|
+
socket.on('error', () => socket.destroy());
|
|
39
|
+
socket.on('close', () => {
|
|
40
|
+
this.closed = true;
|
|
41
|
+
onClose();
|
|
42
|
+
for (const resolve of this.#waiting.values())
|
|
43
|
+
resolve(null);
|
|
44
|
+
this.#waiting.clear();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
static open(host, port, onClose) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const socket = connect({ host, port });
|
|
50
|
+
socket.once('connect', () => resolve(new Dialed(socket, onClose)));
|
|
51
|
+
socket.once('error', () => {
|
|
52
|
+
socket.destroy();
|
|
53
|
+
resolve(null);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
ask(pk, box) {
|
|
58
|
+
if (this.closed)
|
|
59
|
+
return Promise.resolve(null);
|
|
60
|
+
const id = this.#next;
|
|
61
|
+
this.#next = this.#next === MAX_ID ? 1 : this.#next + 1;
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
this.#waiting.set(id, resolve);
|
|
64
|
+
this.#socket.write(askFrame(id, pk, box));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
close() {
|
|
68
|
+
this.#socket.destroy();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class TcpCarrier extends Carrier {
|
|
72
|
+
#routes = new Map();
|
|
73
|
+
#lines = new Map();
|
|
74
|
+
// The address a ward pk is reached at, replacing any before, or null to
|
|
75
|
+
// forget it.
|
|
76
|
+
route(pk, at) {
|
|
77
|
+
if (!isHex(pk, 64) || (at !== null && parseAt(at) === null))
|
|
78
|
+
return false;
|
|
79
|
+
if (at === null)
|
|
80
|
+
this.#routes.delete(pk);
|
|
81
|
+
else
|
|
82
|
+
this.#routes.set(pk, at);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// Nothing where the pk has no route, the address is not reached, or the
|
|
86
|
+
// connection closed before an answer.
|
|
87
|
+
async carry(pk, bytes) {
|
|
88
|
+
const at = this.#routes.get(pk);
|
|
89
|
+
if (at === undefined)
|
|
90
|
+
return null;
|
|
91
|
+
const line = await this.#line(at);
|
|
92
|
+
return line ? line.ask(unhex(pk), bytes) : null;
|
|
93
|
+
}
|
|
94
|
+
#line(at) {
|
|
95
|
+
let line = this.#lines.get(at);
|
|
96
|
+
if (!line) {
|
|
97
|
+
const { host, port } = parseAt(at);
|
|
98
|
+
line = Dialed.open(host, port, () => this.#lines.delete(at)).then((dialed) => {
|
|
99
|
+
if (!dialed)
|
|
100
|
+
this.#lines.delete(at);
|
|
101
|
+
return dialed;
|
|
102
|
+
});
|
|
103
|
+
this.#lines.set(at, line);
|
|
104
|
+
}
|
|
105
|
+
return line;
|
|
106
|
+
}
|
|
107
|
+
async close() {
|
|
108
|
+
const lines = [...this.#lines.values()];
|
|
109
|
+
this.#lines.clear();
|
|
110
|
+
for (const line of await Promise.all(lines))
|
|
111
|
+
line?.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export class TcpListener {
|
|
115
|
+
#server;
|
|
116
|
+
#doors;
|
|
117
|
+
#sockets = new Set();
|
|
118
|
+
constructor(doors) {
|
|
119
|
+
this.#doors = doors;
|
|
120
|
+
this.#server = createServer((socket) => this.#serve(socket));
|
|
121
|
+
}
|
|
122
|
+
// A listener on `host` and `port`, 127.0.0.1 and a free port unless named.
|
|
123
|
+
static listen(doors, host = '127.0.0.1', port = 0) {
|
|
124
|
+
const listener = new TcpListener(doors);
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
listener.#server.once('error', reject);
|
|
127
|
+
listener.#server.listen(port, host, () => resolve(listener));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
get at() {
|
|
131
|
+
const address = this.#server.address();
|
|
132
|
+
return address.family === 'IPv6' ? `[${address.address}]:${address.port}` : `${address.address}:${address.port}`;
|
|
133
|
+
}
|
|
134
|
+
#serve(socket) {
|
|
135
|
+
this.#sockets.add(socket);
|
|
136
|
+
const reader = new FrameReader();
|
|
137
|
+
socket.on('data', (chunk) => {
|
|
138
|
+
for (const f of reader.push(new Uint8Array(chunk))) {
|
|
139
|
+
if (f.kind === 'ask')
|
|
140
|
+
void this.#answer(socket, f.id, f.pk, f.box);
|
|
141
|
+
}
|
|
142
|
+
if (reader.bad)
|
|
143
|
+
socket.destroy();
|
|
144
|
+
});
|
|
145
|
+
socket.on('error', () => socket.destroy());
|
|
146
|
+
socket.on('close', () => this.#sockets.delete(socket));
|
|
147
|
+
}
|
|
148
|
+
async #answer(socket, id, pk, box) {
|
|
149
|
+
let reply = null;
|
|
150
|
+
try {
|
|
151
|
+
reply = await this.#doors.arrive(hex(pk), box);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
reply = null;
|
|
155
|
+
}
|
|
156
|
+
if (!socket.destroyed)
|
|
157
|
+
socket.write(reply === null ? nothingFrame(id) : replyFrame(id, reply));
|
|
158
|
+
}
|
|
159
|
+
close() {
|
|
160
|
+
for (const socket of this.#sockets)
|
|
161
|
+
socket.destroy();
|
|
162
|
+
return new Promise((resolve) => this.#server.close(() => resolve()));
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/ward/index.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export { HEAD_ROW, Partition, type BeingRow, type Head, type StandingRow } from
|
|
|
3
3
|
export { DEPTH } from './stance.ts';
|
|
4
4
|
export { DOCK, Ward, type Catalogue, type Lender, type WardParts } from './ward.ts';
|
|
5
5
|
export { ownerAsk, pilots, WardBeing, type Steward, type WardBeingStance } from './ward-being.ts';
|
|
6
|
+
export { Pilot } from './pilot.ts';
|
package/dist/ward/index.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Being, type AskSpec, type JsonObject } from '../being/index.ts';
|
|
2
|
+
export declare class Pilot extends Being {
|
|
3
|
+
static readonly kind: string;
|
|
4
|
+
static asks: Record<string, AskSpec>;
|
|
5
|
+
hold(args: JsonObject): Promise<JsonObject>;
|
|
6
|
+
pilot(args: JsonObject): Promise<JsonObject>;
|
|
7
|
+
drop(): JsonObject;
|
|
8
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The pilot: a kit being that holds an owner's relation on another ward's
|
|
3
|
+
// ward-being, so whoever pilots her ward pilots that ward from afar. The
|
|
4
|
+
// relation is an ordinary standing, kept in her ward's partition, so it
|
|
5
|
+
// moves its keys and survives a restart like any other.
|
|
6
|
+
//
|
|
7
|
+
// hold { invitation } the owner invitation taken, under `far`
|
|
8
|
+
// pilot { method?, args? } the far ward-being asked, as its owner
|
|
9
|
+
// drop the relation let go
|
|
10
|
+
//
|
|
11
|
+
// She is the kit's, of kind `org.nervur.pilot`, and answers whoever pilots
|
|
12
|
+
// her own ward alone.
|
|
13
|
+
import { Being, isSilence, isWord, told } from '../being/index.js';
|
|
14
|
+
import { readInvitation } from '../quo/index.js';
|
|
15
|
+
import { ownerAsk, PILOT } from './ward-being.js';
|
|
16
|
+
// The standing id the far ward-being is held under.
|
|
17
|
+
const FAR = 'far';
|
|
18
|
+
const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
19
|
+
export class Pilot extends Being {
|
|
20
|
+
static kind = PILOT;
|
|
21
|
+
static asks = {
|
|
22
|
+
hold: ownerAsk('an owner invitation on a far ward, held from now on', { required: ['invitation'] }),
|
|
23
|
+
pilot: ownerAsk('the far ward-being, asked as its owner'),
|
|
24
|
+
drop: ownerAsk('the far ward let go'),
|
|
25
|
+
};
|
|
26
|
+
async hold(args) {
|
|
27
|
+
const invitation = readInvitation(args.invitation);
|
|
28
|
+
if (invitation === null)
|
|
29
|
+
return { error: 'no invitation' };
|
|
30
|
+
if (this.stance.standings.get(FAR))
|
|
31
|
+
return { error: 'a ward is held' };
|
|
32
|
+
return (await this.stance.standings.take(FAR, invitation)) === null ? { error: 'not taken' } : { held: invitation.ward };
|
|
33
|
+
}
|
|
34
|
+
async pilot(args) {
|
|
35
|
+
if ((args.method !== undefined && typeof args.method !== 'string') || (args.args !== undefined && !object(args.args)))
|
|
36
|
+
return { error: 'method is text, args an object' };
|
|
37
|
+
const standing = this.stance.standings.get(FAR);
|
|
38
|
+
if (!standing)
|
|
39
|
+
return { error: 'no ward is held' };
|
|
40
|
+
const out = await standing.ask(args.method, args.args ?? {});
|
|
41
|
+
if (isSilence(out))
|
|
42
|
+
return { error: 'silence' };
|
|
43
|
+
if (isWord(out))
|
|
44
|
+
return { error: told(out) };
|
|
45
|
+
return { answer: out };
|
|
46
|
+
}
|
|
47
|
+
drop() {
|
|
48
|
+
return this.stance.standings.remove(FAR) ? { dropped: true } : { error: 'no ward is held' };
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/ward/stance.d.ts
CHANGED
|
@@ -22,8 +22,6 @@ export declare class WardStance implements Stance {
|
|
|
22
22
|
readonly standings: Standings;
|
|
23
23
|
constructor(inside: Inside, key: string, row: BeingRow);
|
|
24
24
|
get cells(): JsonObject;
|
|
25
|
-
boot(
|
|
26
|
-
lend(contract:
|
|
27
|
-
readonly name: string;
|
|
28
|
-
}, id: string): Promise<string | null>;
|
|
25
|
+
boot(kind: string, key: string, id?: string): Promise<string | null>;
|
|
26
|
+
lend(contract: abstract new (...args: never[]) => unknown, id: string): Promise<string | null>;
|
|
29
27
|
}
|
package/dist/ward/stance.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// standing, one send at a time, inside her allowance.
|
|
5
5
|
import { readValue, writeValue } from '../crypto/index.js';
|
|
6
6
|
import { readInvitation, Standing as Wire, freshStanding } from '../quo/index.js';
|
|
7
|
-
import { OWNER, silence, word } from '../being/index.js';
|
|
7
|
+
import { OWNER, ownKind, silence, word } from '../being/index.js';
|
|
8
8
|
import { allow, Bell, LATE } from './allowance.js';
|
|
9
9
|
import { Cells, DEPTH } from './cells.js';
|
|
10
10
|
export { DEPTH };
|
|
@@ -65,18 +65,19 @@ export class WardStance {
|
|
|
65
65
|
get #live() {
|
|
66
66
|
return this.#inside.live(this.key, this.#row);
|
|
67
67
|
}
|
|
68
|
-
boot(
|
|
69
|
-
return this.#live ? this.#inside.boot(this.key,
|
|
68
|
+
boot(kind, key, id) {
|
|
69
|
+
return this.#live ? this.#inside.boot(this.key, kind, key, id) : Promise.resolve(null);
|
|
70
70
|
}
|
|
71
71
|
async lend(contract, id) {
|
|
72
|
-
|
|
72
|
+
const kind = ownKind(contract);
|
|
73
|
+
if (kind === undefined || !this.#free(id, this.#row.standings))
|
|
73
74
|
return null;
|
|
74
|
-
const invitation = await this.#inside.offer(
|
|
75
|
+
const invitation = await this.#inside.offer(kind);
|
|
75
76
|
if (invitation === null)
|
|
76
77
|
return null;
|
|
77
78
|
const taken = await this.#take(id, invitation);
|
|
78
79
|
if (taken === null)
|
|
79
|
-
await this.#inside.retract(
|
|
80
|
+
await this.#inside.retract(kind, invitation.heir);
|
|
80
81
|
return taken;
|
|
81
82
|
}
|
|
82
83
|
#free(id, of) {
|
|
@@ -18,7 +18,9 @@ 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 PILOT = "org.nervur.pilot";
|
|
21
22
|
export declare class WardBeing extends Being {
|
|
23
|
+
static readonly kind: string;
|
|
22
24
|
static asks: Record<string, AskSpec>;
|
|
23
25
|
readonly steward: Steward;
|
|
24
26
|
constructor(stance: WardBeingStance);
|
package/dist/ward/ward-being.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The ward-being: the first being of every ward, the ward itself. Her asks
|
|
3
3
|
// are the root's, shown and answered to `OWNER` alone.
|
|
4
|
-
import { Being, isSilence, isWord, OWNER, told } from '../being/index.js';
|
|
4
|
+
import { Being, isSilence, isWord, KIT, OWNER, told } from '../being/index.js';
|
|
5
5
|
// Whoever pilots a ward: the root, holding its unsealed ask, or an owner,
|
|
6
6
|
// an occupant of the ward-being the root invited as one.
|
|
7
7
|
export const pilots = (asker, notes) => asker.id === OWNER || notes?.owner === true;
|
|
@@ -10,9 +10,13 @@ export const ownerAsk = (description, input = {}) => ({ description, input: { ty
|
|
|
10
10
|
// An ask the root alone may make, so a carried key pilots but never hands
|
|
11
11
|
// piloting on.
|
|
12
12
|
const rootAsk = (description, input = {}) => ({ description, input: { type: 'object', ...input }, for: (asker) => asker.id === OWNER });
|
|
13
|
+
// The one kit kind the root boots: every other kit class stands where the
|
|
14
|
+
// kit puts it.
|
|
15
|
+
export const PILOT = 'org.nervur.pilot';
|
|
13
16
|
const text = (v) => typeof v === 'string';
|
|
14
17
|
const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
18
|
export class WardBeing extends Being {
|
|
19
|
+
static kind = 'org.nervur.ward-being';
|
|
16
20
|
static asks = {
|
|
17
21
|
boot: ownerAsk('a new being of this ward', { required: ['key', 'class'] }),
|
|
18
22
|
unboot: ownerAsk('a being out, with every relation she holds', { required: ['being'] }),
|
|
@@ -45,6 +49,8 @@ export class WardBeing extends Being {
|
|
|
45
49
|
async boot(args) {
|
|
46
50
|
if (!text(args.key) || !text(args.class))
|
|
47
51
|
return { error: 'key and class are text' };
|
|
52
|
+
if (args.class.startsWith(KIT) && args.class !== PILOT)
|
|
53
|
+
return { error: 'no such class' };
|
|
48
54
|
const out = await this.steward.boot(args.key, args.class);
|
|
49
55
|
return out === 'booted' ? { booted: args.key } : { error: out };
|
|
50
56
|
}
|
package/dist/ward/ward.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Door, WardKey, type Behind, type Choice, type Invitation, type Payload } from '../quo/index.ts';
|
|
2
2
|
import type { Carrier, Clock, Entropy, Rows } from '../contract/index.ts';
|
|
3
|
-
import { type Answer, type BeingClass, type JsonObject } from '../being/index.ts';
|
|
3
|
+
import { type Answer, type BeingClass, type BeingLike, type JsonObject } from '../being/index.ts';
|
|
4
4
|
import { Partition, type BeingRow } from './partition.ts';
|
|
5
5
|
import { type Inside } from './stance.ts';
|
|
6
6
|
import { WardBeing, type WardBeingStance } from './ward-being.ts';
|
|
7
7
|
export interface Catalogue {
|
|
8
|
-
classOf(
|
|
8
|
+
classOf(kind: string): BeingClass | undefined;
|
|
9
9
|
}
|
|
10
10
|
export type WardParts = {
|
|
11
11
|
readonly seed: Uint8Array | string;
|
|
@@ -33,6 +33,8 @@ export declare class Ward implements Behind, Inside {
|
|
|
33
33
|
get pk(): string;
|
|
34
34
|
get draw(): import("../quo/keys.ts").Draw;
|
|
35
35
|
get wardBeing(): WardBeing;
|
|
36
|
+
being(key: string): BeingLike | undefined;
|
|
37
|
+
bearAnew(): void;
|
|
36
38
|
root(method?: string, args?: JsonObject): Promise<Answer>;
|
|
37
39
|
get zero(): boolean;
|
|
38
40
|
answer(heir: string | null, p: Payload): Promise<Choice>;
|