nervur 0.19.2 → 0.20.0
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 +66 -101
- package/dist/being/being.d.ts +7 -15
- package/dist/being/being.js +16 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +9 -0
- package/dist/being/faculty.js +68 -0
- package/dist/being/index.d.ts +4 -5
- package/dist/being/index.js +5 -5
- package/dist/being/types.d.ts +35 -62
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/contract/index.d.ts +26 -0
- package/dist/contract/index.js +19 -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 +13 -0
- package/dist/folder/index.js +125 -0
- package/dist/harbor/dock.d.ts +19 -0
- package/dist/harbor/dock.js +92 -0
- package/dist/harbor/harbor.d.ts +22 -0
- package/dist/harbor/harbor.js +84 -0
- package/dist/harbor/index.d.ts +4 -10
- package/dist/harbor/index.js +5 -18
- package/dist/harbor/registry.d.ts +7 -0
- package/dist/harbor/registry.js +13 -0
- package/dist/harbor/terrain.d.ts +8 -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 +20 -0
- package/dist/pointer/bodies.js +45 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +34 -0
- package/dist/pointer/world.js +76 -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/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 +5 -10
- package/dist/ward/index.js +6 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/stance.d.ts +29 -20
- package/dist/ward/stance.js +173 -406
- package/dist/ward/ward-being.d.ts +33 -0
- package/dist/ward/ward-being.js +76 -0
- package/dist/ward/ward.d.ts +43 -11
- package/dist/ward/ward.js +215 -363
- package/package.json +19 -33
- package/src/being/being.ts +31 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +66 -0
- package/src/being/index.ts +5 -6
- package/src/being/types.ts +47 -145
- package/src/being/words.ts +31 -0
- package/src/contract/index.ts +44 -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 +134 -0
- package/src/harbor/dock.ts +101 -0
- package/src/harbor/harbor.ts +105 -0
- package/src/harbor/index.ts +5 -19
- package/src/harbor/registry.ts +20 -0
- package/src/harbor/terrain.ts +13 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +47 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +91 -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 +19 -0
- package/src/stand/stand.ts +178 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +6 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/stance.ts +185 -420
- package/src/ward/ward-being.ts +97 -0
- package/src/ward/ward.ts +229 -363
- 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,111 @@
|
|
|
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.ts';
|
|
8
|
+
import type { Invitation } from './invitation.ts';
|
|
9
|
+
import { Ephemeral, SigningKey, wardHalves, type Draw } from './keys.ts';
|
|
10
|
+
import { writePayload } from './payload.ts';
|
|
11
|
+
import { readReply, type Reply } from './reply.ts';
|
|
12
|
+
import { checkBody, follow, knockEdge, openReply, sealAsk, signBody, splitBody } from './seal.ts';
|
|
13
|
+
|
|
14
|
+
export type Read = Reply | { readonly nothing: true };
|
|
15
|
+
|
|
16
|
+
// A knock that brought no object back: its bytes, sent again as they are,
|
|
17
|
+
// and what the standing moves to if it bound.
|
|
18
|
+
type Knock = { readonly bytes: string; readonly lid: string; readonly next: string; readonly edge: string; readonly seq: number };
|
|
19
|
+
|
|
20
|
+
export type StandingState = {
|
|
21
|
+
readonly key: string | null;
|
|
22
|
+
readonly edge: string | null;
|
|
23
|
+
readonly sent: number;
|
|
24
|
+
readonly moved: number;
|
|
25
|
+
readonly knock: Knock | null;
|
|
26
|
+
// Whether the last ask after an unanswered knock asked under its own key,
|
|
27
|
+
// so the next sends the knock again.
|
|
28
|
+
readonly probed: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const freshStanding = (): StandingState => ({ key: null, edge: null, sent: 0, moved: 0, knock: null, probed: false });
|
|
32
|
+
|
|
33
|
+
// One ask out: what `read` needs to read its reply and to move.
|
|
34
|
+
export type Sent = { readonly bytes: Uint8Array; readonly lid: Ephemeral; readonly next: string; readonly edge: string; readonly seq: number };
|
|
35
|
+
|
|
36
|
+
export class Standing {
|
|
37
|
+
readonly invitation: Invitation;
|
|
38
|
+
state: StandingState;
|
|
39
|
+
readonly draw: Draw;
|
|
40
|
+
|
|
41
|
+
constructor(invitation: Invitation, state: StandingState, draw: Draw) {
|
|
42
|
+
this.invitation = invitation;
|
|
43
|
+
this.state = state;
|
|
44
|
+
this.draw = draw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async ask(method?: string, args?: string): Promise<Sent | null> {
|
|
48
|
+
const s = this.state;
|
|
49
|
+
if (s.key !== null) return this.#ask(unhex(s.key), unhex(s.edge!), method, args);
|
|
50
|
+
if (s.knock === null) return this.#knock(method, args);
|
|
51
|
+
if (!s.probed) {
|
|
52
|
+
this.state = { ...s, probed: true };
|
|
53
|
+
return this.#ask(unhex(s.knock.next), unhex(s.knock.edge), method, args);
|
|
54
|
+
}
|
|
55
|
+
this.state = { ...s, probed: false };
|
|
56
|
+
// The same bytes again: a new lid would be a new box.
|
|
57
|
+
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 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async read(sent: Sent, bytes: Uint8Array | null): Promise<Read> {
|
|
61
|
+
if (bytes === null) return { nothing: true };
|
|
62
|
+
const reply = await this.#open(sent, bytes);
|
|
63
|
+
if (reply === null) return { silence: true };
|
|
64
|
+
if ('object' in reply.text && sent.seq > this.state.moved) {
|
|
65
|
+
const edge = await follow(unhex(sent.edge), reply.agreement);
|
|
66
|
+
this.state = { ...this.state, key: sent.next, edge: hex(edge), moved: sent.seq, knock: null, probed: false };
|
|
67
|
+
}
|
|
68
|
+
return reply.text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async #knock(method?: string, args?: string): Promise<Sent | null> {
|
|
72
|
+
const heir = await SigningKey.from(unhex(this.invitation.secret));
|
|
73
|
+
const next = await SigningKey.draw(this.draw);
|
|
74
|
+
const lid = await Ephemeral.draw(this.draw);
|
|
75
|
+
const sent = encapsulate(unhex(this.invitation.lock), this.draw(32));
|
|
76
|
+
if (sent === null) return null;
|
|
77
|
+
const edge = await knockEdge(sent.shared);
|
|
78
|
+
const seq = this.state.sent + 1;
|
|
79
|
+
const body = await this.#body(heir, next, seq, method, args);
|
|
80
|
+
const bytes = await sealAsk(lid, unhex(wardHalves(this.invitation.ward).padlock), unhex(this.invitation.heir), edge, body, sent.ciphertext);
|
|
81
|
+
if (bytes === null) return null;
|
|
82
|
+
this.state = { ...this.state, sent: seq, knock: { bytes: hex(bytes), lid: hex(lid.secret), next: hex(next.secret), edge: hex(edge), seq } };
|
|
83
|
+
return { bytes, lid, next: hex(next.secret), edge: hex(edge), seq };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async #ask(secret: Uint8Array, edge: Uint8Array, method?: string, args?: string): Promise<Sent | null> {
|
|
87
|
+
const by = await SigningKey.from(secret);
|
|
88
|
+
const next = await SigningKey.draw(this.draw);
|
|
89
|
+
const lid = await Ephemeral.draw(this.draw);
|
|
90
|
+
const seq = this.state.sent + 1;
|
|
91
|
+
const body = await this.#body(by, next, seq, method, args);
|
|
92
|
+
const bytes = await sealAsk(lid, unhex(wardHalves(this.invitation.ward).padlock), unhex(this.invitation.heir), edge, body);
|
|
93
|
+
if (bytes === null) return null;
|
|
94
|
+
this.state = { ...this.state, sent: seq };
|
|
95
|
+
return { bytes, lid, next: hex(next.secret), edge: hex(edge), seq };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#body(by: SigningKey, next: SigningKey, seq: number, method?: string, args?: string): Promise<Uint8Array> {
|
|
99
|
+
const text = writePayload({ to: this.invitation.heir, by: by.id, next: next.id, seq, ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args }) });
|
|
100
|
+
return signBody(new TextEncoder().encode(text), by);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async #open(sent: Sent, bytes: Uint8Array): Promise<{ text: Reply; agreement: Uint8Array } | null> {
|
|
104
|
+
const opened = await openReply(bytes, sent.lid);
|
|
105
|
+
const body = opened && splitBody(opened.bytes);
|
|
106
|
+
if (!opened || !body) return null;
|
|
107
|
+
if (!(await checkBody(unhex(wardHalves(this.invitation.ward).signing), body))) return null;
|
|
108
|
+
const text = readReply(body.text);
|
|
109
|
+
return text && { text, agreement: opened.agreement };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The stand program: one JSON request a line on stdin, one answer a line on
|
|
3
|
+
// stdout, until stdin closes.
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { Stand } from './stand.ts';
|
|
6
|
+
|
|
7
|
+
const stand = new Stand((n) => globalThis.crypto.getRandomValues(new Uint8Array(n)));
|
|
8
|
+
const pending: Promise<void>[] = [];
|
|
9
|
+
|
|
10
|
+
for await (const line of createInterface({ input: process.stdin, crlfDelay: Infinity })) {
|
|
11
|
+
if (line === '') continue;
|
|
12
|
+
pending.push(
|
|
13
|
+
stand.handle(line).then(
|
|
14
|
+
(answer) => void process.stdout.write(`${JSON.stringify(answer)}\n`),
|
|
15
|
+
(error: unknown) => void process.stderr.write(`${String(error)}\n`),
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
await Promise.all(pending);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The stand of `quo/vectors/HARNESS.md`, part one, over `quo/` alone. Part
|
|
3
|
+
// two carries bytes over TCP, which is the dock's, so its three requests
|
|
4
|
+
// are `bad request`.
|
|
5
|
+
import { hex, isHex, unhex } from '../crypto/index.ts';
|
|
6
|
+
import { Door, freshStanding, MemoryRelations, readInvitation, Standing, WardKey, type Behind, type Choice, type Draw, type Invitation, type Payload, type Read, type Sent } from '../quo/index.ts';
|
|
7
|
+
|
|
8
|
+
type Answer = Record<string, unknown>;
|
|
9
|
+
const ERRORS = ['bad request', 'no such op', 'no such ward', 'not reached', 'ward stood', 'name held'] as const;
|
|
10
|
+
type Error = (typeof ERRORS)[number];
|
|
11
|
+
class Refused {
|
|
12
|
+
readonly error: Error;
|
|
13
|
+
constructor(error: Error) {
|
|
14
|
+
this.error = error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const refuse = (error: Error): never => {
|
|
18
|
+
throw new Refused(error);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// What the verifier names behind a door.
|
|
22
|
+
const REACHES: Record<string, (p: Payload) => Choice> = {
|
|
23
|
+
echo: (p) => ({ object: p.method === undefined ? '{}' : (p.args ?? '{}'), seen: null }),
|
|
24
|
+
marked: (p) => ({ object: p.method === undefined ? '{}' : (p.args ?? '{}'), seen: p.method === undefined ? null : '1' }),
|
|
25
|
+
null: () => ({ object: 'null', seen: null }),
|
|
26
|
+
silent: () => ({ silence: true }),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class StandWard implements Behind {
|
|
30
|
+
readonly door: Door;
|
|
31
|
+
readonly zero: boolean;
|
|
32
|
+
readonly #zero: string | undefined;
|
|
33
|
+
readonly names = new Map<string, string>();
|
|
34
|
+
readonly reaches = new Map<string, string>();
|
|
35
|
+
readonly standings = new Map<string, { standing: Standing; last?: Sent }>();
|
|
36
|
+
|
|
37
|
+
constructor(key: WardKey, zero: string | undefined, draw: Draw) {
|
|
38
|
+
this.#zero = zero;
|
|
39
|
+
this.zero = zero !== undefined;
|
|
40
|
+
this.door = new Door(key, new MemoryRelations(), this, draw);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
answer(heir: string | null, p: Payload): Promise<Choice> {
|
|
44
|
+
const reach = heir === null ? this.#zero! : this.reaches.get(heir);
|
|
45
|
+
return Promise.resolve(reach === undefined ? { silence: true } : REACHES[reach]!(p));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const OPS = new Set(['ward', 'invite', 'release', 'arrive', 'ask', 'read', 'listen', 'route', 'send']);
|
|
50
|
+
|
|
51
|
+
export class Stand {
|
|
52
|
+
readonly #wards = new Map<string, StandWard>();
|
|
53
|
+
readonly #draw: Draw;
|
|
54
|
+
|
|
55
|
+
constructor(draw: Draw) {
|
|
56
|
+
this.#draw = draw;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async handle(line: string): Promise<Answer> {
|
|
60
|
+
let request: unknown;
|
|
61
|
+
try {
|
|
62
|
+
request = JSON.parse(line);
|
|
63
|
+
} catch {
|
|
64
|
+
return { id: null, error: 'bad request' };
|
|
65
|
+
}
|
|
66
|
+
if (typeof request !== 'object' || request === null || Array.isArray(request)) return { id: null, error: 'bad request' };
|
|
67
|
+
const r = request as Record<string, unknown>;
|
|
68
|
+
if (typeof r.id !== 'string') return { id: null, error: 'bad request' };
|
|
69
|
+
try {
|
|
70
|
+
return { id: r.id, ...(await this.#op(r)) };
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (e instanceof Refused) return { id: r.id, error: e.error };
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async #op(r: Record<string, unknown>): Promise<Answer> {
|
|
78
|
+
if (typeof r.op !== 'string') return refuse('bad request');
|
|
79
|
+
if (!OPS.has(r.op)) return refuse('no such op');
|
|
80
|
+
switch (r.op) {
|
|
81
|
+
case 'ward':
|
|
82
|
+
return this.#ward(text(r.seed), optional(r.reach));
|
|
83
|
+
case 'invite':
|
|
84
|
+
return this.#invite(r);
|
|
85
|
+
case 'release':
|
|
86
|
+
return this.#release(r);
|
|
87
|
+
case 'arrive':
|
|
88
|
+
return this.#arrive(r);
|
|
89
|
+
case 'ask':
|
|
90
|
+
return this.#ask(r);
|
|
91
|
+
case 'read':
|
|
92
|
+
return this.#read(r);
|
|
93
|
+
default:
|
|
94
|
+
return refuse('bad request');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async #ward(seed: string, reach: string | undefined): Promise<Answer> {
|
|
99
|
+
reached(reach);
|
|
100
|
+
const key = await WardKey.from(seed);
|
|
101
|
+
if (this.#wards.has(key.pk)) refuse('ward stood');
|
|
102
|
+
this.#wards.set(key.pk, new StandWard(key, reach, this.#draw));
|
|
103
|
+
return { ward: key.pk };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async #invite(r: Record<string, unknown>): Promise<Answer> {
|
|
107
|
+
const name = text(r.heir);
|
|
108
|
+
const reach = optional(r.reach) ?? 'echo';
|
|
109
|
+
const ward = this.#ward_(r.ward);
|
|
110
|
+
reached(reach);
|
|
111
|
+
if (ward.names.has(name)) refuse('name held');
|
|
112
|
+
const { heir, invitation } = await ward.door.invite();
|
|
113
|
+
ward.names.set(name, heir);
|
|
114
|
+
ward.reaches.set(heir, reach);
|
|
115
|
+
return { invitation };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#release(r: Record<string, unknown>): Answer {
|
|
119
|
+
const name = text(r.heir);
|
|
120
|
+
const ward = this.#ward_(r.ward);
|
|
121
|
+
const heir = ward.names.get(name);
|
|
122
|
+
if (heir === undefined) return { released: null };
|
|
123
|
+
ward.names.delete(name);
|
|
124
|
+
ward.door.remove(heir);
|
|
125
|
+
return { released: name };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async #arrive(r: Record<string, unknown>): Promise<Answer> {
|
|
129
|
+
const box = r.box;
|
|
130
|
+
if (!isHex(box)) return refuse('bad request');
|
|
131
|
+
const ward = this.#ward_(r.ward);
|
|
132
|
+
const judged = await ward.door.arrive(unhex(box));
|
|
133
|
+
return { reply: hex(judged.bytes) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async #ask(r: Record<string, unknown>): Promise<Answer> {
|
|
137
|
+
const invitation = inviteOf(r.invitation);
|
|
138
|
+
const method = r.method === undefined ? undefined : text(r.method);
|
|
139
|
+
const args = r.args === undefined ? undefined : objectText(r.args);
|
|
140
|
+
const ward = this.#ward_(r.ward);
|
|
141
|
+
const held = this.#standing(ward, invitation);
|
|
142
|
+
const sent = await held.standing.ask(method, args);
|
|
143
|
+
if (sent === null) return refuse('bad request');
|
|
144
|
+
held.last = sent;
|
|
145
|
+
return { box: hex(sent.bytes) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async #read(r: Record<string, unknown>): Promise<Answer> {
|
|
149
|
+
const invitation = inviteOf(r.invitation);
|
|
150
|
+
if (r.reply !== null && !isHex(r.reply)) return refuse('bad request');
|
|
151
|
+
const ward = this.#ward_(r.ward);
|
|
152
|
+
const held = this.#standing(ward, invitation);
|
|
153
|
+
if (held.last === undefined) return refuse('bad request');
|
|
154
|
+
const read = await held.standing.read(held.last, r.reply === null ? null : unhex(r.reply as string));
|
|
155
|
+
return { read: readOf(read) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#standing(ward: StandWard, invitation: Invitation): { standing: Standing; last?: Sent } {
|
|
159
|
+
const key = `${invitation.ward}.${invitation.heir}`;
|
|
160
|
+
let held = ward.standings.get(key);
|
|
161
|
+
if (!held) ward.standings.set(key, (held = { standing: new Standing(invitation, freshStanding(), this.#draw) }));
|
|
162
|
+
return held;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#ward_(pk: unknown): StandWard {
|
|
166
|
+
if (!isHex(pk, 64)) return refuse('bad request');
|
|
167
|
+
return this.#wards.get(pk) ?? refuse('no such ward');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const text = (value: unknown): string => (typeof value === 'string' ? value : refuse('bad request'));
|
|
172
|
+
const optional = (value: unknown): string | undefined => (value === undefined ? undefined : text(value));
|
|
173
|
+
const reached = (reach: string | undefined): void => {
|
|
174
|
+
if (reach !== undefined && !Object.hasOwn(REACHES, reach)) refuse('not reached');
|
|
175
|
+
};
|
|
176
|
+
const inviteOf = (value: unknown): Invitation => readInvitation(value) ?? refuse('bad request');
|
|
177
|
+
const objectText = (value: unknown): string => (typeof value === 'object' && value !== null && !Array.isArray(value) ? JSON.stringify(value) : refuse('bad request'));
|
|
178
|
+
const readOf = (read: Read): unknown => ('object' in read ? { object: JSON.parse(read.object) as unknown, seen: read.seen } : read);
|
package/src/ward/allowance.ts
CHANGED
|
@@ -1,85 +1,47 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The allowance
|
|
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.
|
|
2
|
+
// The allowance: how long a being waits on one ask. It is on no wire. A
|
|
3
|
+
// being who says nothing gets the default, and nobody gets past the
|
|
4
|
+
// ceiling.
|
|
5
|
+
import type { Clock } from '../contract/index.ts';
|
|
6
|
+
import type { Wanted } from '../being/index.ts';
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import type { Wanted } from '../being/types.ts';
|
|
8
|
+
export const DEFAULT = 30_000;
|
|
9
|
+
export const CEILING = 300_000;
|
|
18
10
|
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
export const allow = (wanted: Wanted | undefined): number => {
|
|
12
|
+
const whole = typeof wanted?.time === 'number' ? Math.floor(wanted.time) : NaN;
|
|
13
|
+
return Math.min(Number.isFinite(whole) && whole > 0 ? whole : DEFAULT, CEILING);
|
|
14
|
+
};
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// refused. A being who never learns the third parameter exists writes correct
|
|
25
|
-
// code forever under these. They are two numbers because the third argument
|
|
26
|
-
// goes both ways: less for the ask she wants back quickly, more for the one
|
|
27
|
-
// she knows is slow.
|
|
28
|
-
export const DEFAULT: Allowance = { time: 30_000 };
|
|
29
|
-
export const CEILING: Allowance = { time: 300_000 };
|
|
16
|
+
export const LATE: unique symbol = Symbol('late');
|
|
17
|
+
export type Late = typeof LATE;
|
|
30
18
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// whole one.
|
|
39
|
-
const whole = (n: unknown, fallback: number) => {
|
|
40
|
-
const w = typeof n === 'number' ? Math.floor(n) : NaN;
|
|
41
|
-
return Number.isFinite(w) && w > 0 ? w : fallback;
|
|
42
|
-
};
|
|
43
|
-
return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
|
|
44
|
-
}
|
|
19
|
+
// One ask's bell. It bounds the wait of the being who asked and the send
|
|
20
|
+
// that holds the relation's line, so a far side that never answers holds
|
|
21
|
+
// neither for longer than the allowance.
|
|
22
|
+
export class Bell {
|
|
23
|
+
readonly #wait: { done: Promise<void>; cancel(): void };
|
|
24
|
+
readonly #late: Promise<Late>;
|
|
25
|
+
#rang = false;
|
|
45
26
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
constructor(clock: Clock, ms: number) {
|
|
28
|
+
this.#wait = clock.wait(ms);
|
|
29
|
+
this.#late = this.#wait.done.then((): Late => {
|
|
30
|
+
this.#rang = true;
|
|
31
|
+
return LATE;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
53
34
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
35
|
+
get rang(): boolean {
|
|
36
|
+
return this.#rang;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
race<T>(work: Promise<T>): Promise<T | Late> {
|
|
40
|
+
work.catch(() => {});
|
|
41
|
+
return Promise.race([work, this.#late]);
|
|
42
|
+
}
|
|
57
43
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// stopped at its own lane, before a single byte is sealed, and a bound that
|
|
61
|
-
// only watched the wire would never see it. A timer is the only way to stop
|
|
62
|
-
// waiting and every language that runs Quo has one; no runtime is named here.
|
|
63
|
-
//
|
|
64
|
-
// `rang` is called the moment the bell rings, before anyone waiting on the
|
|
65
|
-
// wait is told: whoever holds work that has not started yet reads it and does
|
|
66
|
-
// not start it. What comes back late is not read, and what has not left yet
|
|
67
|
-
// does not leave.
|
|
68
|
-
export async function within<T>(ms: number, work: Promise<T>, rang?: () => void): Promise<T | typeof LATE> {
|
|
69
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
-
const bell = new Promise<typeof LATE>((ring) => {
|
|
71
|
-
timer = setTimeout(() => {
|
|
72
|
-
rang?.();
|
|
73
|
-
ring(LATE);
|
|
74
|
-
}, ms);
|
|
75
|
-
});
|
|
76
|
-
try {
|
|
77
|
-
return await Promise.race([work, bell]);
|
|
78
|
-
} finally {
|
|
79
|
-
clearTimeout(timer);
|
|
80
|
-
void work.then(
|
|
81
|
-
() => {},
|
|
82
|
-
() => {},
|
|
83
|
-
); // whatever it was, it is nobody's answer now
|
|
44
|
+
cancel(): void {
|
|
45
|
+
this.#wait.cancel();
|
|
84
46
|
}
|
|
85
47
|
}
|