@quo-systems/quo 0.1.1 → 0.2.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/SPEC.md +259 -129
- package/dist/being/index.d.ts +1 -1
- package/dist/being/index.js +1 -1
- package/dist/being/silence.d.ts +6 -1
- package/dist/being/silence.js +25 -7
- package/dist/being/types.d.ts +8 -4
- package/dist/being/types.js +1 -1
- package/dist/conformance/beings.d.ts +1 -1
- package/dist/conformance/beings.js +5 -5
- package/dist/conformance/estate.js +23 -34
- package/dist/conformance/index.js +14 -12
- package/dist/harbor/core.d.ts +7 -1
- package/dist/harbor/core.js +25 -2
- package/dist/harbor/dial.js +7 -1
- package/dist/harbor/memory.d.ts +5 -1
- package/dist/harbor/memory.js +7 -2
- package/dist/ward/door.d.ts +6 -2
- package/dist/ward/door.js +55 -35
- package/dist/ward/ground.d.ts +4 -1
- package/dist/ward/heirs.d.ts +4 -2
- package/dist/ward/heirs.js +20 -3
- package/dist/ward/owner.d.ts +5 -3
- package/dist/ward/owner.js +55 -16
- package/dist/ward/partition.d.ts +6 -0
- package/dist/ward/partition.js +2 -0
- package/dist/ward/seal.d.ts +3 -1
- package/dist/ward/seal.js +6 -2
- package/dist/ward/stance.d.ts +2 -2
- package/dist/ward/stance.js +21 -12
- package/dist/ward/ward.js +37 -12
- package/package.json +19 -31
- package/src/being/index.ts +1 -1
- package/src/being/silence.ts +26 -8
- package/src/being/types.ts +13 -3
- package/src/conformance/beings.ts +5 -5
- package/src/conformance/estate.ts +22 -36
- package/src/conformance/index.ts +14 -12
- package/src/harbor/core.ts +24 -2
- package/src/harbor/dial.ts +7 -1
- package/src/harbor/memory.ts +6 -1
- package/src/ward/door.ts +54 -32
- package/src/ward/ground.ts +6 -2
- package/src/ward/heirs.ts +20 -5
- package/src/ward/owner.ts +54 -16
- package/src/ward/partition.ts +10 -0
- package/src/ward/seal.ts +8 -5
- package/src/ward/stance.ts +20 -14
- package/src/ward/ward.ts +37 -13
- package/vectors/framing.json +7 -1
package/dist/ward/door.js
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Judgment at the door. Sealed bytes in, sealed bytes out
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
2
|
+
// Judgment at the door. Sealed bytes in, sealed bytes out, and one bit for
|
|
3
|
+
// the harbor: whether a key this door holds spoke. A stranger, bytes the
|
|
4
|
+
// door cannot admit, meets one silence whatever the case, so that it learns
|
|
5
|
+
// nothing, not even which case it hit. A key the door has bound hears the
|
|
6
|
+
// ward's word for what happened, sealed to its own lid, because it has
|
|
7
|
+
// already proven who it is and a reason to it is an oracle to nobody. What
|
|
8
|
+
// passes is named and dispatched to her answer, and her digest for that
|
|
9
|
+
// asker rides back with the object.
|
|
10
|
+
import { isSilence, isWord } from '../being/silence.js';
|
|
7
11
|
import { digest } from '../being/digest.js';
|
|
8
12
|
import { at } from './partition.js';
|
|
9
13
|
import { openAsk, sealReply, verifyAsk } from './seal.js';
|
|
10
14
|
import { spent } from './allowance.js';
|
|
11
15
|
import { KEY, sealingPair } from './arithmetic.js';
|
|
12
16
|
const SILENCE = { silence: true };
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const said = (quo) => ({ quo });
|
|
18
|
+
// One arrival at one being, already named. Catches every throw. `bound` says
|
|
19
|
+
// whether the asker is a key this door holds: she hears `threw`; a stranger
|
|
20
|
+
// at the public being hears silence, because her insides are hers.
|
|
21
|
+
export async function arrive(door, asker, method, args, bound) {
|
|
22
|
+
const threw = bound ? said('threw') : SILENCE;
|
|
15
23
|
let out;
|
|
16
24
|
try {
|
|
17
25
|
out = await door.being.answer(asker, method, args);
|
|
18
26
|
}
|
|
19
27
|
catch {
|
|
20
|
-
return
|
|
28
|
+
return threw; // she threw where she was asked. there is no answer.
|
|
21
29
|
}
|
|
22
30
|
// Nothing at all is not an answer either: a method that forgot to return
|
|
23
31
|
// has said nothing, and nothing is silence, never a value that JSON drops
|
|
24
32
|
// on the way out and the far side reads back as a fourth word.
|
|
25
|
-
if (out === undefined || isSilence(out)
|
|
33
|
+
if (out === undefined || isSilence(out))
|
|
26
34
|
return SILENCE;
|
|
35
|
+
if (isWord(out))
|
|
36
|
+
return threw; // a word is the ward's to say, never hers
|
|
27
37
|
if (method === undefined)
|
|
28
38
|
return { object: out, seen: null };
|
|
29
39
|
// The digest rides along, it is not the answer. She has already answered:
|
|
@@ -33,7 +43,7 @@ export async function arrive(door, asker, method, args) {
|
|
|
33
43
|
async function seen(door, asker) {
|
|
34
44
|
try {
|
|
35
45
|
const bp = await door.being.answer(asker);
|
|
36
|
-
return isSilence(bp) ||
|
|
46
|
+
return isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
37
47
|
}
|
|
38
48
|
catch {
|
|
39
49
|
return null;
|
|
@@ -43,41 +53,50 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
43
53
|
const judge = async (bytes) => {
|
|
44
54
|
const a = await openAsk(bytes, key.padlock);
|
|
45
55
|
if (!a)
|
|
46
|
-
return null; // it did not open. there is nobody to answer.
|
|
56
|
+
return null; // D1. it did not open. there is nobody to answer.
|
|
47
57
|
const { to, payload, ephemeralPk } = a;
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
if (spent({ time: payload.time }))
|
|
53
|
-
return
|
|
54
|
-
// An ask with no doors left is one silence like a budget already gone.
|
|
55
|
-
// Nothing sets hops and nothing decrements it yet; this door refuses zero
|
|
56
|
-
// so that a relay chain invented later meets doors that already stop it.
|
|
57
|
-
if (payload.hops === 0)
|
|
58
|
-
return { reply: SILENCE, ephemeralPk };
|
|
58
|
+
const refuse = () => ({ reply: SILENCE, ephemeralPk, heard: false });
|
|
59
|
+
// D2, the rest of it. The allowance is read before anything is done under
|
|
60
|
+
// it, and hops at zero is refused so that a relay chain invented later
|
|
61
|
+
// meets doors that already stop it. Nothing sets hops.
|
|
62
|
+
if (spent({ time: payload.time }) || payload.hops === 0)
|
|
63
|
+
return refuse();
|
|
59
64
|
const args = payload.args ?? {};
|
|
60
65
|
if (to === null) {
|
|
66
|
+
// For nobody: the public being, or D3 nobody home; D4 the signature
|
|
67
|
+
// fails under the key the payload names. She is asked by strangers,
|
|
68
|
+
// and a stranger hears silence for whatever she then does.
|
|
61
69
|
const pk = publicKey();
|
|
62
70
|
const pub = pk !== null ? doors.get(pk) : undefined;
|
|
63
71
|
if (!pub || !(await verifyAsk(a, payload.by)))
|
|
64
|
-
return
|
|
65
|
-
return { reply: await arrive(pub, {}, payload.method, args), ephemeralPk };
|
|
72
|
+
return refuse();
|
|
73
|
+
return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: true };
|
|
66
74
|
}
|
|
67
75
|
const h = heirs.admits(to, payload.by);
|
|
68
|
-
if (!h)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
if (!h) {
|
|
77
|
+
// D5 a heir not held, D6 a key not admitted. One more case: the heir
|
|
78
|
+
// was held and the id removed. Its last keys are kept so that their
|
|
79
|
+
// holder, and only their holder, D7 checked, hears that she is gone.
|
|
80
|
+
if (!heirs.gone(to, payload.by) || !(await verifyAsk(a, payload.by)))
|
|
81
|
+
return refuse();
|
|
82
|
+
return { reply: said('removed'), ephemeralPk, heard: true };
|
|
83
|
+
}
|
|
73
84
|
if (!(await verifyAsk(a, payload.by)))
|
|
74
|
-
return
|
|
85
|
+
return refuse(); // D7, under an admitted key
|
|
86
|
+
// From here the door has heard a key it holds. Every answer below is
|
|
87
|
+
// sealed to that key's lid; nothing below is a stranger's.
|
|
88
|
+
const door = doors.get(h.being);
|
|
89
|
+
if (!door)
|
|
90
|
+
return { reply: said('absent'), ephemeralPk, heard: true }; // D8. she did not come back this run
|
|
91
|
+
if (!at(door.cells.occupants, h.id))
|
|
92
|
+
return { reply: said('removed'), ephemeralPk, heard: true }; // D8. the record is gone
|
|
75
93
|
// Once only, and only now: the number is spent after the signature, so a
|
|
76
94
|
// stranger cannot burn a number she could not sign for, and together with
|
|
77
95
|
// the keys, so the same bytes twice rotate nothing and refusal writes nothing.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
const honoured = heirs.honour(h, payload.by, payload.next, payload.seq); // D9, D10
|
|
97
|
+
if (honoured !== true)
|
|
98
|
+
return { reply: said(honoured), ephemeralPk, heard: true };
|
|
99
|
+
return { reply: await arrive(door, { id: h.id }, payload.method, args, true), ephemeralPk, heard: true };
|
|
81
100
|
};
|
|
82
101
|
// The door itself. Always answers bytes. When the ask did not open, the
|
|
83
102
|
// reply is sealed to the ephemeral pk on its lid, which is all a stranger
|
|
@@ -89,11 +108,12 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
89
108
|
return async function door(bytes) {
|
|
90
109
|
const out = await judge(bytes);
|
|
91
110
|
const reply = out?.reply ?? SILENCE;
|
|
111
|
+
const heard = out?.heard ?? false;
|
|
92
112
|
try {
|
|
93
|
-
return await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32));
|
|
113
|
+
return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
|
|
94
114
|
}
|
|
95
115
|
catch {
|
|
96
|
-
return sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32));
|
|
116
|
+
return { bytes: await sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
|
|
97
117
|
}
|
|
98
118
|
};
|
|
99
119
|
}
|
package/dist/ward/ground.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export type Ground = {
|
|
|
7
7
|
random(n: number): Uint8Array;
|
|
8
8
|
};
|
|
9
9
|
export type WardPointers = {
|
|
10
|
-
door(bytes: Uint8Array): Promise<
|
|
10
|
+
door(bytes: Uint8Array): Promise<{
|
|
11
|
+
bytes: Uint8Array;
|
|
12
|
+
heard: boolean;
|
|
13
|
+
}>;
|
|
11
14
|
ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
12
15
|
};
|
package/dist/ward/heirs.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DoorWord } from '../being/types.ts';
|
|
2
|
+
import { type Heir, type Partition } from './partition.ts';
|
|
2
3
|
export declare class Heirs {
|
|
3
4
|
#private;
|
|
4
5
|
constructor(p: Partition);
|
|
5
6
|
open(heir: string, being: string, id: string): void;
|
|
6
7
|
close(heir: string): void;
|
|
8
|
+
gone(heir: string, by: string): boolean;
|
|
7
9
|
get(heir: string): Heir | undefined;
|
|
8
10
|
admits(heir: string, by: string): Heir | null;
|
|
9
11
|
spend(h: Heir, seq: number): boolean;
|
|
10
|
-
honour(h: Heir, by: string, next: string | null, seq: number):
|
|
12
|
+
honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord;
|
|
11
13
|
settle(h: Heir, by: string, next: string | null): boolean;
|
|
12
14
|
}
|
package/dist/ward/heirs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GONE } from './partition.js';
|
|
1
2
|
// How wide the span is, is the ward's own — wider is more forgiving of a
|
|
2
3
|
// rough road, and no peer can tell the difference except by being refused.
|
|
3
4
|
const SPAN = 64;
|
|
@@ -9,8 +10,24 @@ export class Heirs {
|
|
|
9
10
|
open(heir, being, id) {
|
|
10
11
|
this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
|
|
11
12
|
}
|
|
13
|
+
// The id was removed. The heir is forgotten, and its last keys are kept
|
|
14
|
+
// apart, bounded, so that whoever still holds them hears `removed` at the
|
|
15
|
+
// door, once they have signed as themselves, and nobody else hears a thing.
|
|
12
16
|
close(heir) {
|
|
17
|
+
const h = this.#p.heirs[heir];
|
|
18
|
+
if (!h)
|
|
19
|
+
return;
|
|
13
20
|
delete this.#p.heirs[heir];
|
|
21
|
+
this.#p.gone[heir] = { current: h.current, announced: h.announced };
|
|
22
|
+
const keys = Object.keys(this.#p.gone);
|
|
23
|
+
for (const old of keys.slice(0, Math.max(0, keys.length - GONE)))
|
|
24
|
+
delete this.#p.gone[old];
|
|
25
|
+
}
|
|
26
|
+
// May `by` speak for a relation she removed? True only for the keys the
|
|
27
|
+
// door held when the id went, which nobody but their holder has.
|
|
28
|
+
gone(heir, by) {
|
|
29
|
+
const g = this.#p.gone[heir];
|
|
30
|
+
return !!g && (by === g.current || by === g.announced);
|
|
14
31
|
}
|
|
15
32
|
get(heir) {
|
|
16
33
|
return this.#p.heirs[heir];
|
|
@@ -60,10 +77,10 @@ export class Heirs {
|
|
|
60
77
|
// behind her. Every write below this line is one that is going to hold.
|
|
61
78
|
honour(h, by, next, seq) {
|
|
62
79
|
if (h.fresh && next === null)
|
|
63
|
-
return
|
|
80
|
+
return 'unannounced'; // a knock without a key of her own binds nothing
|
|
64
81
|
if (!this.spend(h, seq))
|
|
65
|
-
return
|
|
66
|
-
return this.settle(h, by, next);
|
|
82
|
+
return 'repeated';
|
|
83
|
+
return this.settle(h, by, next) ? true : 'unannounced';
|
|
67
84
|
}
|
|
68
85
|
// The signature checked out. Settle the keys: a fresh heir rotates at once
|
|
69
86
|
// to what it announced and must announce something; a current key replaces
|
package/dist/ward/owner.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
|
|
1
|
+
import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
|
|
2
2
|
export type OwnerSide = {
|
|
3
3
|
pk: string;
|
|
4
4
|
doors: Map<string, {
|
|
@@ -24,9 +24,11 @@ export type OwnerSide = {
|
|
|
24
24
|
};
|
|
25
25
|
}>;
|
|
26
26
|
publicKey(): string | null;
|
|
27
|
-
instantiate(key: string, className: string
|
|
27
|
+
instantiate(key: string, className: string): {
|
|
28
28
|
key: string;
|
|
29
29
|
} | null;
|
|
30
|
+
unboot(key: string): string[] | null;
|
|
31
|
+
setPublic(key: string): void;
|
|
30
32
|
};
|
|
31
|
-
export declare const OWNER_ASKS:
|
|
33
|
+
export declare const OWNER_ASKS: Ask[];
|
|
32
34
|
export declare function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json>;
|
package/dist/ward/owner.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The owner's asks. The root owner holds the ask pointer; every other owner
|
|
3
|
-
// is an occupant of the ward itself, invited by the root.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
3
|
+
// is an occupant of the ward itself, invited by the root. The ward is a
|
|
4
|
+
// being to her owner: the empty ask is her describe, and it carries her asks
|
|
5
|
+
// with a description and an input each, so a side renders them as it
|
|
6
|
+
// renders any being's. Six asks: boot, public, invite, knock, remove and
|
|
7
|
+
// unboot, which is boot's inverse and takes her relations with her. The
|
|
8
|
+
// knock is a being's knock made for her, and the standing is written into
|
|
9
|
+
// her cells under the id the owner gave; remove takes a relation out of her
|
|
10
|
+
// by id, the mirror of it.
|
|
11
|
+
import { isSilence, isWord, wordOf } from '../being/silence.js';
|
|
8
12
|
import { digest } from '../being/digest.js';
|
|
9
13
|
import { OWNER } from '../being/types.js';
|
|
10
|
-
|
|
14
|
+
const str = { type: 'string' };
|
|
15
|
+
export const OWNER_ASKS = [
|
|
16
|
+
{ name: 'boot', description: 'boot a being by class name, under a key the owner chooses', input: { type: 'object', properties: { key: str, class: str }, required: ['key', 'class'] } },
|
|
17
|
+
{ name: 'public', description: 'mark a booted being as the one public being of the ward, reached by anyone at the bare pk', input: { type: 'object', properties: { key: str }, required: ['key'] } },
|
|
18
|
+
{ name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
|
|
19
|
+
{
|
|
20
|
+
name: 'knock',
|
|
21
|
+
description: 'knock for a being of the ward with an invitation, and take the standing under id if answered; being is a key booted, or { boot: class, key } to boot her first',
|
|
22
|
+
input: { type: 'object', properties: { being: { description: 'a key, or { boot, key }' }, id: str, invitation: { type: 'object' }, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } }, required: ['being', 'id', 'invitation'] },
|
|
23
|
+
},
|
|
24
|
+
{ name: 'remove', description: 'take a relation out of a being of the ward by id, occupant or standing; on the ward pk it unseats an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
|
|
25
|
+
{ name: 'unboot', description: 'take a being out of the ward, with every relation she holds; her occupants hear removed, and the ward itself is refused', input: { type: 'object', properties: { being: str }, required: ['being'] } },
|
|
26
|
+
];
|
|
11
27
|
export async function ownerAnswer(w, asker, method, args) {
|
|
12
28
|
// Every name the owner gives is a word. Coerced instead of checked, an
|
|
13
29
|
// object passed where a name belongs becomes the string "[object Object]",
|
|
@@ -16,17 +32,24 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
16
32
|
if (method === undefined)
|
|
17
33
|
return describe(w);
|
|
18
34
|
if (method === 'boot') {
|
|
19
|
-
// A ward may have one public being, and no more. Marking a second would
|
|
20
|
-
// leave the first holding every relation she had, reachable by nobody at
|
|
21
|
-
// the bare pk, and told by nobody that she had been replaced.
|
|
22
|
-
if (args.public === true && w.publicKey() !== null)
|
|
23
|
-
return { error: 'a ward has one public being' };
|
|
24
35
|
const key = word(args.key), className = word(args.class);
|
|
25
36
|
if (key === null || className === null)
|
|
26
37
|
return { error: 'no such class, or key taken' };
|
|
27
|
-
const door = w.instantiate(key, className
|
|
38
|
+
const door = w.instantiate(key, className);
|
|
28
39
|
return door ? { booted: door.key } : { error: 'no such class, or key taken' };
|
|
29
40
|
}
|
|
41
|
+
if (method === 'public') {
|
|
42
|
+
// A ward may have one public being, and no more. Marking a second would
|
|
43
|
+
// leave the first holding every relation she had, reachable by nobody at
|
|
44
|
+
// the bare pk, and told by nobody that she had been replaced.
|
|
45
|
+
const key = word(args.key);
|
|
46
|
+
if (key === null || key === w.pk || !w.doors.has(key))
|
|
47
|
+
return { error: 'no such being' };
|
|
48
|
+
if (w.publicKey() !== null && w.publicKey() !== key)
|
|
49
|
+
return { error: 'a ward has one public being' };
|
|
50
|
+
w.setPublic(key);
|
|
51
|
+
return { public: key };
|
|
52
|
+
}
|
|
30
53
|
if (method === 'invite') {
|
|
31
54
|
// On the ward itself, only the root may invite: an occupant of the ward
|
|
32
55
|
// is an owner, and ownership is minted by the ask pointer alone. An owner
|
|
@@ -63,8 +86,10 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
63
86
|
// The owner is a caller like any other and may say what this knock may
|
|
64
87
|
// spend. Saying nothing is the ward's default, as it is for a being.
|
|
65
88
|
const out = await door.stance.standings.knock(inv, args.method, args.args ?? {}, args.wanted);
|
|
66
|
-
if (isSilence(out)
|
|
67
|
-
return { error:
|
|
89
|
+
if (isSilence(out))
|
|
90
|
+
return { error: 'silence' };
|
|
91
|
+
if (isWord(out))
|
|
92
|
+
return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
|
|
68
93
|
return { taken: await door.stance.standings.take(id, inv), answer: out };
|
|
69
94
|
}
|
|
70
95
|
if (method === 'remove') {
|
|
@@ -88,6 +113,20 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
88
113
|
return { error: 'nothing to remove' };
|
|
89
114
|
return { removed: id };
|
|
90
115
|
}
|
|
116
|
+
if (method === 'unboot') {
|
|
117
|
+
// The inverse of boot, and the only way a being leaves a ward. Her
|
|
118
|
+
// relations go with her, so an occupant of hers hears `removed` from the
|
|
119
|
+
// keys the door kept, exactly as it would had the owner removed each id
|
|
120
|
+
// by hand; the owner hears which ids those were. The ward itself is
|
|
121
|
+
// refused, for the reason knock is: it is a being to nobody outside, and
|
|
122
|
+
// unbooting it would be a ward deleting itself from inside its own map,
|
|
123
|
+
// leaving its owners bound to a door that is gone.
|
|
124
|
+
const being = word(args.being);
|
|
125
|
+
if (being === null || being === w.pk)
|
|
126
|
+
return { error: 'no such being' };
|
|
127
|
+
const removed = w.unboot(being);
|
|
128
|
+
return removed === null ? { error: 'no such being' } : { unbooted: being, removed };
|
|
129
|
+
}
|
|
91
130
|
return { error: 'unknown ask' };
|
|
92
131
|
}
|
|
93
132
|
// The ward's describe for its owner: its beings, their classes, a digest each,
|
|
@@ -100,7 +139,7 @@ async function describe(w) {
|
|
|
100
139
|
let d = null;
|
|
101
140
|
try {
|
|
102
141
|
const bp = await door.being.answer({ id: OWNER });
|
|
103
|
-
d = isSilence(bp) ||
|
|
142
|
+
d = isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
104
143
|
}
|
|
105
144
|
catch {
|
|
106
145
|
// she threw where she was asked. there is no blueprint, and d stays null.
|
|
@@ -108,7 +147,7 @@ async function describe(w) {
|
|
|
108
147
|
beings[key] = { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d };
|
|
109
148
|
}
|
|
110
149
|
return {
|
|
111
|
-
asks: OWNER_ASKS
|
|
150
|
+
asks: OWNER_ASKS,
|
|
112
151
|
notes: { pk: w.pk, beings },
|
|
113
152
|
};
|
|
114
153
|
}
|
package/dist/ward/partition.d.ts
CHANGED
|
@@ -34,6 +34,11 @@ export type Bind = {
|
|
|
34
34
|
answered: Record<string, true>;
|
|
35
35
|
minted: string[];
|
|
36
36
|
};
|
|
37
|
+
export type Gone = {
|
|
38
|
+
current: string;
|
|
39
|
+
announced: string | null;
|
|
40
|
+
};
|
|
41
|
+
export declare const GONE = 256;
|
|
37
42
|
export type Partition = {
|
|
38
43
|
version: string;
|
|
39
44
|
beings: Record<string, Cells & {
|
|
@@ -41,6 +46,7 @@ export type Partition = {
|
|
|
41
46
|
}>;
|
|
42
47
|
bind: Record<string, Bind>;
|
|
43
48
|
heirs: Record<string, Heir>;
|
|
49
|
+
gone: Record<string, Gone>;
|
|
44
50
|
public: string | null;
|
|
45
51
|
};
|
|
46
52
|
export declare function open(memory: Record<string, unknown>): Partition;
|
package/dist/ward/partition.js
CHANGED
|
@@ -25,6 +25,7 @@ export const READS = [VERSION];
|
|
|
25
25
|
function upgrade(p) {
|
|
26
26
|
return p;
|
|
27
27
|
}
|
|
28
|
+
export const GONE = 256;
|
|
28
29
|
// The one place a ward reads state that another ward wrote. A fresh
|
|
29
30
|
// memory becomes a partition of this version; anything already written must
|
|
30
31
|
// say it is this version, and a ward that cannot read it does not boot. Birth is
|
|
@@ -41,6 +42,7 @@ export function open(memory) {
|
|
|
41
42
|
p.beings ??= {};
|
|
42
43
|
p.bind ??= {};
|
|
43
44
|
p.heirs ??= {};
|
|
45
|
+
p.gone ??= {};
|
|
44
46
|
p.public ??= null;
|
|
45
47
|
return p;
|
|
46
48
|
}
|
package/dist/ward/seal.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Json, JsonObject } from '../being/types.ts';
|
|
1
|
+
import type { DoorWord, Json, JsonObject } from '../being/types.ts';
|
|
2
2
|
export type WardKey = {
|
|
3
3
|
sign: Uint8Array;
|
|
4
4
|
padlock: Uint8Array;
|
|
@@ -28,6 +28,8 @@ export type ReplyPayload = {
|
|
|
28
28
|
seen: string | null;
|
|
29
29
|
} | {
|
|
30
30
|
silence: true;
|
|
31
|
+
} | {
|
|
32
|
+
quo: DoorWord;
|
|
31
33
|
};
|
|
32
34
|
export declare function sealAsk(to: string | null, payload: AskPayload, signer: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{
|
|
33
35
|
bytes: Uint8Array;
|
package/dist/ward/seal.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDoorWord } from '../being/silence.js';
|
|
1
2
|
import { KEY, SIGNATURE, box, concat, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.js';
|
|
2
3
|
const utf8 = new TextEncoder();
|
|
3
4
|
const text = new TextDecoder();
|
|
@@ -73,8 +74,9 @@ export async function sealReply(reply, ephemeralPk, wardSign, seed) {
|
|
|
73
74
|
// Open a reply with the ephemeral secret kept from the ask, and verify it
|
|
74
75
|
// came from the ward it was sent to. Null is silence: it did not open, it
|
|
75
76
|
// lied, or it is not a reply. Every ward on the wire is somebody else's code,
|
|
76
|
-
// so the shape is checked here and nowhere later: a reply is `{ silence }
|
|
77
|
-
//
|
|
77
|
+
// so the shape is checked here and nowhere later: a reply is `{ silence }`,
|
|
78
|
+
// `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
|
|
79
|
+
// object present and seen a digest or null.
|
|
78
80
|
// Anything else is bytes that said nothing, and her ward never hands her a
|
|
79
81
|
// throw or an absent value for what a stranger wrote.
|
|
80
82
|
export async function openReply(bytes, ephemeralSecret, signPk) {
|
|
@@ -93,6 +95,8 @@ export async function openReply(bytes, ephemeralSecret, signPk) {
|
|
|
93
95
|
const r = reply;
|
|
94
96
|
if (r.silence === true)
|
|
95
97
|
return { silence: true };
|
|
98
|
+
if (Object.hasOwn(r, 'quo'))
|
|
99
|
+
return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
96
100
|
if (!Object.hasOwn(r, 'object') || r.object === undefined)
|
|
97
101
|
return null;
|
|
98
102
|
if (r.seen !== null && !(typeof r.seen === 'string' && /^[0-9a-f]{64}$/.test(r.seen)))
|
package/dist/ward/stance.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Cells, JsonObject, Silence, Stance,
|
|
1
|
+
import type { Cells, JsonObject, Silence, Stance, Wanted, Word } from '../being/types.ts';
|
|
2
2
|
import { type Bind, type StandingKeys } from './partition.ts';
|
|
3
3
|
import { type ReplyPayload } from './seal.ts';
|
|
4
4
|
export type Inside = {
|
|
@@ -9,7 +9,7 @@ export type Inside = {
|
|
|
9
9
|
}>;
|
|
10
10
|
openHeir(heir: string, being: string, id: string): void;
|
|
11
11
|
closeHeir(heir: string): void;
|
|
12
|
-
send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence |
|
|
12
|
+
send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
|
|
13
13
|
};
|
|
14
14
|
export type SealedInvitation = {
|
|
15
15
|
ward: string;
|
package/dist/ward/stance.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The one stance builder. Used for every being the ward boots. Nothing
|
|
3
3
|
// outer is in what she holds: ids in, values out, keys in the bind table.
|
|
4
|
-
import { silence, isSilence, isUnreached } from '../being/silence.js';
|
|
4
|
+
import { silence, isSilence, isUnreached, isWord, word } from '../being/silence.js';
|
|
5
5
|
import { digest } from '../being/digest.js';
|
|
6
6
|
import { RESERVED_IDS } from '../being/types.js';
|
|
7
7
|
import { at, put, drop } from './partition.js';
|
|
@@ -65,14 +65,16 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
65
65
|
const out = await within(allow(wanted).time, work(() => !late), () => {
|
|
66
66
|
late = true;
|
|
67
67
|
});
|
|
68
|
-
return out === LATE ?
|
|
68
|
+
return out === LATE ? word('late') : out;
|
|
69
69
|
};
|
|
70
70
|
const send = async (keys, method, args, wanted, rec) => {
|
|
71
71
|
const r = await inside.send(bind, keys, true, method, args, wanted);
|
|
72
|
-
if (isSilence(r) ||
|
|
72
|
+
if (isSilence(r) || isWord(r))
|
|
73
73
|
return r;
|
|
74
74
|
if ('silence' in r)
|
|
75
75
|
return silence;
|
|
76
|
+
if ('quo' in r)
|
|
77
|
+
return word(r.quo); // the far door's word for her, sealed to her lid
|
|
76
78
|
if (rec && method !== undefined && r.seen !== null)
|
|
77
79
|
rec.seen = r.seen; // the digest rode along
|
|
78
80
|
return r.object;
|
|
@@ -105,7 +107,7 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
105
107
|
k.next = own.next;
|
|
106
108
|
if (isUnreached(r))
|
|
107
109
|
return r; // still nothing. she knows no more than before.
|
|
108
|
-
if (!isSilence(r) && !('
|
|
110
|
+
if (!isSilence(r) && !isWord(r) && 'object' in r) {
|
|
109
111
|
k.spoke = true; // it was heard the first time, and this is the answer
|
|
110
112
|
bind.answered[name] = true;
|
|
111
113
|
return r.object;
|
|
@@ -127,9 +129,16 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
127
129
|
}
|
|
128
130
|
if (isUnreached(r))
|
|
129
131
|
return r; // nothing came back. she does not know whether it was heard.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
// The heir is spent once the door honoured it, whatever she then said: an
|
|
133
|
+
// object, silence, or that she threw. A refusal wrote nothing, and says
|
|
134
|
+
// so by its word, so the heir still speaks and the next knock is as it.
|
|
135
|
+
k.spoke = !(!isSilence(r) && !isWord(r) && 'quo' in r && r.quo !== 'threw');
|
|
136
|
+
if (isSilence(r) || isWord(r))
|
|
137
|
+
return r;
|
|
138
|
+
if ('silence' in r)
|
|
132
139
|
return silence;
|
|
140
|
+
if ('quo' in r)
|
|
141
|
+
return word(r.quo);
|
|
133
142
|
bind.answered[name] = true;
|
|
134
143
|
return r.object;
|
|
135
144
|
};
|
|
@@ -141,15 +150,15 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
141
150
|
const calls = {
|
|
142
151
|
knock: async (inv, method, args = {}, wanted) => {
|
|
143
152
|
if (!valid(inv))
|
|
144
|
-
return
|
|
153
|
+
return word('invitation'); // S1. nothing is sent
|
|
145
154
|
// She may knock again, and after take that knock is an ask: the relation
|
|
146
155
|
// has one home, so it answers on the standing's lane, under the
|
|
147
156
|
// standing's keys and its count. A second lane here would speak for the
|
|
148
157
|
// same relation, and the two would refuse each other.
|
|
149
158
|
const id = takenAs(nameOf(inv));
|
|
150
159
|
if (id !== undefined)
|
|
151
|
-
return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, bind.standings[id], cells.standings[id], method, args, wanted) : Promise.resolve(
|
|
152
|
-
return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(
|
|
160
|
+
return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, bind.standings[id], cells.standings[id], method, args, wanted) : Promise.resolve(word('late')))));
|
|
161
|
+
return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
|
|
153
162
|
},
|
|
154
163
|
// Take consumes. Until now the relation lived in the knock record, under
|
|
155
164
|
// the ward and the heir; from now it lives in the standing, under her id.
|
|
@@ -201,14 +210,14 @@ export function buildStance(inside, key, cells, bind) {
|
|
|
201
210
|
ask: (method, args = {}, wanted) => {
|
|
202
211
|
const keys = at(bind.standings, id);
|
|
203
212
|
if (!keys)
|
|
204
|
-
return Promise.resolve(
|
|
213
|
+
return Promise.resolve(word('dropped')); // S2. she dropped it between one line and the next
|
|
205
214
|
const rec = at(cells.standings, id);
|
|
206
|
-
return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(
|
|
215
|
+
return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(word('late')))));
|
|
207
216
|
},
|
|
208
217
|
});
|
|
209
218
|
const asking = async (id, keys, rec, method, args, wanted) => {
|
|
210
219
|
const out = await send(keys, method, args, wanted, rec);
|
|
211
|
-
if (method === undefined && !isSilence(out) && !
|
|
220
|
+
if (method === undefined && !isSilence(out) && !isWord(out)) {
|
|
212
221
|
const here = at(cells.standings, id);
|
|
213
222
|
if (here) {
|
|
214
223
|
here.blueprint = out;
|