nervur 0.19.2 → 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 +156 -104
- package/dist/being/being.d.ts +8 -15
- package/dist/being/being.js +20 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +10 -0
- package/dist/being/faculty.js +74 -0
- package/dist/being/index.d.ts +5 -5
- package/dist/being/index.js +6 -5
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +35 -63
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- 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 +39 -0
- package/dist/contract/index.js +33 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +14 -0
- package/dist/folder/index.js +87 -0
- package/dist/harbor/catalogue.d.ts +25 -0
- package/dist/harbor/catalogue.js +93 -0
- package/dist/harbor/dock.d.ts +29 -0
- package/dist/harbor/dock.js +122 -0
- package/dist/harbor/harbor.d.ts +31 -0
- package/dist/harbor/harbor.js +146 -0
- package/dist/harbor/index.d.ts +7 -10
- package/dist/harbor/index.js +9 -18
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +115 -0
- package/dist/harbor/registry.d.ts +9 -0
- package/dist/harbor/registry.js +78 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +36 -0
- package/dist/harbor/terrain.d.ts +9 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +26 -0
- package/dist/pointer/bodies.js +64 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +35 -0
- package/dist/pointer/world.js +85 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- 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/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +6 -10
- package/dist/ward/index.js +7 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +27 -20
- package/dist/ward/stance.js +174 -406
- package/dist/ward/ward-being.d.ts +35 -0
- package/dist/ward/ward-being.js +82 -0
- package/dist/ward/ward.d.ts +45 -11
- package/dist/ward/ward.js +239 -357
- package/package.json +24 -31
- package/src/being/being.ts +35 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +73 -0
- package/src/being/index.ts +6 -6
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +50 -146
- package/src/being/words.ts +31 -0
- 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 +73 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +92 -0
- package/src/harbor/catalogue.ts +110 -0
- package/src/harbor/dock.ts +137 -0
- package/src/harbor/harbor.ts +180 -0
- package/src/harbor/index.ts +9 -19
- package/src/harbor/package.ts +131 -0
- package/src/harbor/registry.ts +73 -0
- package/src/harbor/root-line.ts +36 -0
- package/src/harbor/terrain.ts +15 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +65 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +94 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +20 -0
- package/src/stand/stand.ts +220 -0
- package/src/tcp/frame.ts +78 -0
- package/src/tcp/index.ts +173 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +7 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +186 -420
- package/src/ward/ward-being.ts +103 -0
- package/src/ward/ward.ts +256 -359
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Invitation } from './invitation.ts';
|
|
2
|
+
import { Ephemeral, type Draw } from './keys.ts';
|
|
3
|
+
import { type Reply } from './reply.ts';
|
|
4
|
+
export type Read = Reply | {
|
|
5
|
+
readonly nothing: true;
|
|
6
|
+
};
|
|
7
|
+
type Knock = {
|
|
8
|
+
readonly bytes: string;
|
|
9
|
+
readonly lid: string;
|
|
10
|
+
readonly next: string;
|
|
11
|
+
readonly edge: string;
|
|
12
|
+
readonly seq: number;
|
|
13
|
+
};
|
|
14
|
+
export type StandingState = {
|
|
15
|
+
readonly key: string | null;
|
|
16
|
+
readonly edge: string | null;
|
|
17
|
+
readonly sent: number;
|
|
18
|
+
readonly moved: number;
|
|
19
|
+
readonly knock: Knock | null;
|
|
20
|
+
readonly probed: boolean;
|
|
21
|
+
};
|
|
22
|
+
export declare const freshStanding: () => StandingState;
|
|
23
|
+
export type Sent = {
|
|
24
|
+
readonly bytes: Uint8Array;
|
|
25
|
+
readonly lid: Ephemeral;
|
|
26
|
+
readonly next: string;
|
|
27
|
+
readonly edge: string;
|
|
28
|
+
readonly seq: number;
|
|
29
|
+
};
|
|
30
|
+
export declare class Standing {
|
|
31
|
+
#private;
|
|
32
|
+
readonly invitation: Invitation;
|
|
33
|
+
state: StandingState;
|
|
34
|
+
readonly draw: Draw;
|
|
35
|
+
constructor(invitation: Invitation, state: StandingState, draw: Draw);
|
|
36
|
+
ask(method?: string, args?: string): Promise<Sent | null>;
|
|
37
|
+
read(sent: Sent, bytes: Uint8Array | null): Promise<Read>;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The standing's side of a relation: it seals asks and reads replies, and
|
|
3
|
+
// moves to its announced key and the following edge key when an object
|
|
4
|
+
// comes back to an ask above every ask it has moved on, and on nothing else.
|
|
5
|
+
//
|
|
6
|
+
// Its state is a value, so a ward keeps it in its partition.
|
|
7
|
+
import { encapsulate, hex, unhex } from '../crypto/index.js';
|
|
8
|
+
import { Ephemeral, SigningKey, wardHalves } from './keys.js';
|
|
9
|
+
import { writePayload } from './payload.js';
|
|
10
|
+
import { readReply } from './reply.js';
|
|
11
|
+
import { checkBody, follow, knockEdge, openReply, sealAsk, signBody, splitBody } from './seal.js';
|
|
12
|
+
export const freshStanding = () => ({ key: null, edge: null, sent: 0, moved: 0, knock: null, probed: false });
|
|
13
|
+
export class Standing {
|
|
14
|
+
invitation;
|
|
15
|
+
state;
|
|
16
|
+
draw;
|
|
17
|
+
constructor(invitation, state, draw) {
|
|
18
|
+
this.invitation = invitation;
|
|
19
|
+
this.state = state;
|
|
20
|
+
this.draw = draw;
|
|
21
|
+
}
|
|
22
|
+
async ask(method, args) {
|
|
23
|
+
const s = this.state;
|
|
24
|
+
if (s.key !== null)
|
|
25
|
+
return this.#ask(unhex(s.key), unhex(s.edge), method, args);
|
|
26
|
+
if (s.knock === null)
|
|
27
|
+
return this.#knock(method, args);
|
|
28
|
+
if (!s.probed) {
|
|
29
|
+
this.state = { ...s, probed: true };
|
|
30
|
+
return this.#ask(unhex(s.knock.next), unhex(s.knock.edge), method, args);
|
|
31
|
+
}
|
|
32
|
+
this.state = { ...s, probed: false };
|
|
33
|
+
// The same bytes again: a new lid would be a new box.
|
|
34
|
+
return { bytes: unhex(s.knock.bytes), lid: await Ephemeral.from(unhex(s.knock.lid)), next: s.knock.next, edge: s.knock.edge, seq: s.knock.seq };
|
|
35
|
+
}
|
|
36
|
+
async read(sent, bytes) {
|
|
37
|
+
if (bytes === null)
|
|
38
|
+
return { nothing: true };
|
|
39
|
+
const reply = await this.#open(sent, bytes);
|
|
40
|
+
if (reply === null)
|
|
41
|
+
return { silence: true };
|
|
42
|
+
if ('object' in reply.text && sent.seq > this.state.moved) {
|
|
43
|
+
const edge = await follow(unhex(sent.edge), reply.agreement);
|
|
44
|
+
this.state = { ...this.state, key: sent.next, edge: hex(edge), moved: sent.seq, knock: null, probed: false };
|
|
45
|
+
}
|
|
46
|
+
return reply.text;
|
|
47
|
+
}
|
|
48
|
+
async #knock(method, args) {
|
|
49
|
+
const heir = await SigningKey.from(unhex(this.invitation.secret));
|
|
50
|
+
const next = await SigningKey.draw(this.draw);
|
|
51
|
+
const lid = await Ephemeral.draw(this.draw);
|
|
52
|
+
const sent = encapsulate(unhex(this.invitation.lock), this.draw(32));
|
|
53
|
+
if (sent === null)
|
|
54
|
+
return null;
|
|
55
|
+
const edge = await knockEdge(sent.shared);
|
|
56
|
+
const seq = this.state.sent + 1;
|
|
57
|
+
const body = await this.#body(heir, next, seq, method, args);
|
|
58
|
+
const bytes = await sealAsk(lid, unhex(wardHalves(this.invitation.ward).padlock), unhex(this.invitation.heir), edge, body, sent.ciphertext);
|
|
59
|
+
if (bytes === null)
|
|
60
|
+
return null;
|
|
61
|
+
this.state = { ...this.state, sent: seq, knock: { bytes: hex(bytes), lid: hex(lid.secret), next: hex(next.secret), edge: hex(edge), seq } };
|
|
62
|
+
return { bytes, lid, next: hex(next.secret), edge: hex(edge), seq };
|
|
63
|
+
}
|
|
64
|
+
async #ask(secret, edge, method, args) {
|
|
65
|
+
const by = await SigningKey.from(secret);
|
|
66
|
+
const next = await SigningKey.draw(this.draw);
|
|
67
|
+
const lid = await Ephemeral.draw(this.draw);
|
|
68
|
+
const seq = this.state.sent + 1;
|
|
69
|
+
const body = await this.#body(by, next, seq, method, args);
|
|
70
|
+
const bytes = await sealAsk(lid, unhex(wardHalves(this.invitation.ward).padlock), unhex(this.invitation.heir), edge, body);
|
|
71
|
+
if (bytes === null)
|
|
72
|
+
return null;
|
|
73
|
+
this.state = { ...this.state, sent: seq };
|
|
74
|
+
return { bytes, lid, next: hex(next.secret), edge: hex(edge), seq };
|
|
75
|
+
}
|
|
76
|
+
#body(by, next, seq, method, args) {
|
|
77
|
+
const text = writePayload({ to: this.invitation.heir, by: by.id, next: next.id, seq, ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args }) });
|
|
78
|
+
return signBody(new TextEncoder().encode(text), by);
|
|
79
|
+
}
|
|
80
|
+
async #open(sent, bytes) {
|
|
81
|
+
const opened = await openReply(bytes, sent.lid);
|
|
82
|
+
const body = opened && splitBody(opened.bytes);
|
|
83
|
+
if (!opened || !body)
|
|
84
|
+
return null;
|
|
85
|
+
if (!(await checkBody(unhex(wardHalves(this.invitation.ward).signing), body)))
|
|
86
|
+
return null;
|
|
87
|
+
const text = readReply(body.text);
|
|
88
|
+
return text && { text, agreement: opened.agreement };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -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/allowance.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const CEILING: Allowance;
|
|
7
|
-
export declare function allow(wanted: Wanted | undefined, ceiling?: Allowance, base?: Allowance): Allowance;
|
|
8
|
-
export declare const spent: (a: Allowance) => boolean;
|
|
1
|
+
import type { Clock } from '../contract/index.ts';
|
|
2
|
+
import type { Wanted } from '../being/index.ts';
|
|
3
|
+
export declare const DEFAULT = 30000;
|
|
4
|
+
export declare const CEILING = 300000;
|
|
5
|
+
export declare const allow: (wanted: Wanted | undefined) => number;
|
|
9
6
|
export declare const LATE: unique symbol;
|
|
10
|
-
export
|
|
7
|
+
export type Late = typeof LATE;
|
|
8
|
+
export declare class Bell {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(clock: Clock, ms: number);
|
|
11
|
+
get rang(): boolean;
|
|
12
|
+
race<T>(work: Promise<T>): Promise<T | Late>;
|
|
13
|
+
cancel(): void;
|
|
14
|
+
}
|
package/dist/ward/allowance.js
CHANGED
|
@@ -1,71 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// refusal there. The sender bounds the whole of her ask to the same number,
|
|
8
|
-
// and a wait that ran out is the word late, never silence and never unreached.
|
|
9
|
-
//
|
|
10
|
-
// Each ask is bounded on its own. The time an arriving call has left does not
|
|
11
|
-
// bound the asks a being makes while answering it: attributing her onward ask
|
|
12
|
-
// to the arrival that caused it needs a context the ward cannot get without
|
|
13
|
-
// naming a runtime, and the ward names none. Every wait still ends.
|
|
14
|
-
// How wide the default is, and how wide she may ask for, are the ward's own —
|
|
15
|
-
// wider is more patient, and no peer can tell the difference except by being
|
|
16
|
-
// refused. A being who never learns the third parameter exists writes correct
|
|
17
|
-
// code forever under these. They are two numbers because the third argument
|
|
18
|
-
// goes both ways: less for the ask she wants back quickly, more for the one
|
|
19
|
-
// she knows is slow.
|
|
20
|
-
export const DEFAULT = { time: 30_000 };
|
|
21
|
-
export const CEILING = { time: 300_000 };
|
|
22
|
-
// What she asked for, held to what her ward allows. A being cannot mint budget
|
|
23
|
-
// nobody granted her: asking for a minute where the ward gives thirty seconds
|
|
24
|
-
// is thirty seconds, silently, because the ceiling is not hers to know.
|
|
25
|
-
// Asking for nothing at all is the default, which is the whole point.
|
|
26
|
-
export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
|
|
27
|
-
// Floored first, then tested: a fraction of a millisecond is not a budget,
|
|
28
|
-
// and falls to the default like every other number that is not a positive
|
|
29
|
-
// whole one.
|
|
30
|
-
const whole = (n, fallback) => {
|
|
31
|
-
const w = typeof n === 'number' ? Math.floor(n) : NaN;
|
|
32
|
-
return Number.isFinite(w) && w > 0 ? w : fallback;
|
|
33
|
-
};
|
|
34
|
-
return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
|
|
35
|
-
}
|
|
36
|
-
// Whether a budget has anything left to spend, and the one reader of it: the
|
|
37
|
-
// door calls this on arrival and nothing else asks the question anywhere. A
|
|
38
|
-
// time that is not a whole number above zero is not a budget that ran out, it
|
|
39
|
-
// is no budget at all, and the door refuses both the same way, since a peer
|
|
40
|
-
// learns nothing from the difference. `allow` never makes one, so only bytes
|
|
41
|
-
// off the road ever carry one.
|
|
42
|
-
export const spent = (a) => !(Number.isSafeInteger(a.time) && a.time > 0);
|
|
43
|
-
// What a wait that ran out comes back as. Its own value, held by nobody
|
|
44
|
-
// outside the ward, so no answer from any door can be mistaken for it.
|
|
1
|
+
export const DEFAULT = 30_000;
|
|
2
|
+
export const CEILING = 300_000;
|
|
3
|
+
export const allow = (wanted) => {
|
|
4
|
+
const whole = typeof wanted?.time === 'number' ? Math.floor(wanted.time) : NaN;
|
|
5
|
+
return Math.min(Number.isFinite(whole) && whole > 0 ? whole : DEFAULT, CEILING);
|
|
6
|
+
};
|
|
45
7
|
export const LATE = Symbol('late');
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
8
|
+
// One ask's bell. It bounds the wait of the being who asked and the send
|
|
9
|
+
// that holds the relation's line, so a far side that never answers holds
|
|
10
|
+
// neither for longer than the allowance.
|
|
11
|
+
export class Bell {
|
|
12
|
+
#wait;
|
|
13
|
+
#late;
|
|
14
|
+
#rang = false;
|
|
15
|
+
constructor(clock, ms) {
|
|
16
|
+
this.#wait = clock.wait(ms);
|
|
17
|
+
this.#late = this.#wait.done.then(() => {
|
|
18
|
+
this.#rang = true;
|
|
19
|
+
return LATE;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
get rang() {
|
|
23
|
+
return this.#rang;
|
|
24
|
+
}
|
|
25
|
+
race(work) {
|
|
26
|
+
work.catch(() => { });
|
|
27
|
+
return Promise.race([work, this.#late]);
|
|
66
28
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
void work.then(() => { }, () => { }); // whatever it was, it is nobody's answer now
|
|
29
|
+
cancel() {
|
|
30
|
+
this.#wait.cancel();
|
|
70
31
|
}
|
|
71
32
|
}
|
package/dist/ward/cells.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { JsonObject } from '../being/index.ts';
|
|
2
2
|
export declare const DEPTH = 64;
|
|
3
|
-
export declare
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
export declare class Cells {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(wrote: () => void);
|
|
6
|
+
guard(cells: JsonObject): JsonObject;
|
|
7
|
+
}
|