@quo-systems/quo 0.1.1 → 0.2.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/SPEC.md +185 -114
- 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 +12 -6
- package/dist/conformance/index.js +14 -12
- package/dist/harbor/core.d.ts +6 -1
- package/dist/harbor/core.js +11 -2
- 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.js +6 -4
- 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 +7 -7
- 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 +11 -6
- package/src/conformance/index.ts +14 -12
- package/src/harbor/core.ts +11 -2
- 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 +4 -3
- 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 +9 -9
- package/vectors/framing.json +7 -1
package/src/ward/door.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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.ts';
|
|
7
11
|
import { digest } from '../being/digest.ts';
|
|
8
|
-
import type { Asker, BeingLike, JsonObject } from '../being/types.ts';
|
|
12
|
+
import type { Asker, BeingLike, DoorWord, JsonObject } from '../being/types.ts';
|
|
9
13
|
import { at } from './partition.ts';
|
|
10
14
|
import type { Heirs } from './heirs.ts';
|
|
11
15
|
import { openAsk, sealReply, verifyAsk, type ReplyPayload, type WardKey } from './seal.ts';
|
|
@@ -13,20 +17,26 @@ import { spent } from './allowance.ts';
|
|
|
13
17
|
import { KEY, sealingPair } from './arithmetic.ts';
|
|
14
18
|
|
|
15
19
|
export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, unknown> } };
|
|
20
|
+
export type Judged = { bytes: Uint8Array; heard: boolean };
|
|
16
21
|
const SILENCE: ReplyPayload = { silence: true };
|
|
22
|
+
const said = (quo: DoorWord): ReplyPayload => ({ quo });
|
|
17
23
|
|
|
18
|
-
// One arrival at one being, already named. Catches every throw.
|
|
19
|
-
|
|
24
|
+
// One arrival at one being, already named. Catches every throw. `bound` says
|
|
25
|
+
// whether the asker is a key this door holds: she hears `threw`; a stranger
|
|
26
|
+
// at the public being hears silence, because her insides are hers.
|
|
27
|
+
export async function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
|
|
28
|
+
const threw = bound ? said('threw') : SILENCE;
|
|
20
29
|
let out: Awaited<ReturnType<BeingLike['answer']>>;
|
|
21
30
|
try {
|
|
22
31
|
out = await door.being.answer(asker, method, args);
|
|
23
32
|
} catch {
|
|
24
|
-
return
|
|
33
|
+
return threw; // she threw where she was asked. there is no answer.
|
|
25
34
|
}
|
|
26
35
|
// Nothing at all is not an answer either: a method that forgot to return
|
|
27
36
|
// has said nothing, and nothing is silence, never a value that JSON drops
|
|
28
37
|
// on the way out and the far side reads back as a fourth word.
|
|
29
|
-
if (out === undefined || isSilence(out)
|
|
38
|
+
if (out === undefined || isSilence(out)) return SILENCE;
|
|
39
|
+
if (isWord(out)) return threw; // a word is the ward's to say, never hers
|
|
30
40
|
if (method === undefined) return { object: out, seen: null };
|
|
31
41
|
// The digest rides along, it is not the answer. She has already answered:
|
|
32
42
|
// a describe that will not run costs the digest, and nothing else.
|
|
@@ -36,43 +46,54 @@ export async function arrive(door: Door, asker: Asker, method: string | undefine
|
|
|
36
46
|
async function seen(door: Door, asker: Asker): Promise<string | null> {
|
|
37
47
|
try {
|
|
38
48
|
const bp = await door.being.answer(asker);
|
|
39
|
-
return isSilence(bp) ||
|
|
49
|
+
return isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
40
50
|
} catch {
|
|
41
51
|
return null;
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
type Verdict = { reply: ReplyPayload; ephemeralPk: Uint8Array; heard: boolean };
|
|
56
|
+
|
|
45
57
|
export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array) {
|
|
46
|
-
const judge = async (bytes: Uint8Array): Promise<
|
|
58
|
+
const judge = async (bytes: Uint8Array): Promise<Verdict | null> => {
|
|
47
59
|
const a = await openAsk(bytes, key.padlock);
|
|
48
|
-
if (!a) return null; // it did not open. there is nobody to answer.
|
|
60
|
+
if (!a) return null; // D1. it did not open. there is nobody to answer.
|
|
49
61
|
const { to, payload, ephemeralPk } = a;
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
if (spent({ time: payload.time })
|
|
55
|
-
// An ask with no doors left is one silence like a budget already gone.
|
|
56
|
-
// Nothing sets hops and nothing decrements it yet; this door refuses zero
|
|
57
|
-
// so that a relay chain invented later meets doors that already stop it.
|
|
58
|
-
if (payload.hops === 0) return { reply: SILENCE, ephemeralPk };
|
|
62
|
+
const refuse = (): Verdict => ({ reply: SILENCE, ephemeralPk, heard: false });
|
|
63
|
+
// D2, the rest of it. The allowance is read before anything is done under
|
|
64
|
+
// it, and hops at zero is refused so that a relay chain invented later
|
|
65
|
+
// meets doors that already stop it. Nothing sets hops.
|
|
66
|
+
if (spent({ time: payload.time }) || payload.hops === 0) return refuse();
|
|
59
67
|
const args = payload.args ?? {};
|
|
60
68
|
if (to === null) {
|
|
69
|
+
// For nobody: the public being, or D3 nobody home; D4 the signature
|
|
70
|
+
// fails under the key the payload names. She is asked by strangers,
|
|
71
|
+
// and a stranger hears silence for whatever she then does.
|
|
61
72
|
const pk = publicKey();
|
|
62
73
|
const pub = pk !== null ? doors.get(pk) : undefined;
|
|
63
|
-
if (!pub || !(await verifyAsk(a, payload.by))) return
|
|
64
|
-
return { reply: await arrive(pub, {}, payload.method, args), ephemeralPk };
|
|
74
|
+
if (!pub || !(await verifyAsk(a, payload.by))) return refuse();
|
|
75
|
+
return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: true };
|
|
65
76
|
}
|
|
66
77
|
const h = heirs.admits(to, payload.by);
|
|
67
|
-
if (!h)
|
|
78
|
+
if (!h) {
|
|
79
|
+
// D5 a heir not held, D6 a key not admitted. One more case: the heir
|
|
80
|
+
// was held and the id removed. Its last keys are kept so that their
|
|
81
|
+
// holder, and only their holder, D7 checked, hears that she is gone.
|
|
82
|
+
if (!heirs.gone(to, payload.by) || !(await verifyAsk(a, payload.by))) return refuse();
|
|
83
|
+
return { reply: said('removed'), ephemeralPk, heard: true };
|
|
84
|
+
}
|
|
85
|
+
if (!(await verifyAsk(a, payload.by))) 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.
|
|
68
88
|
const door = doors.get(h.being);
|
|
69
|
-
if (!door
|
|
70
|
-
if (!(
|
|
89
|
+
if (!door) return { reply: said('absent'), ephemeralPk, heard: true }; // D8. she did not come back this run
|
|
90
|
+
if (!at(door.cells.occupants, h.id)) return { reply: said('removed'), ephemeralPk, heard: true }; // D8. the record is gone
|
|
71
91
|
// Once only, and only now: the number is spent after the signature, so a
|
|
72
92
|
// stranger cannot burn a number she could not sign for, and together with
|
|
73
93
|
// the keys, so the same bytes twice rotate nothing and refusal writes nothing.
|
|
74
|
-
|
|
75
|
-
|
|
94
|
+
const honoured = heirs.honour(h, payload.by, payload.next, payload.seq); // D9, D10
|
|
95
|
+
if (honoured !== true) return { reply: said(honoured), ephemeralPk, heard: true };
|
|
96
|
+
return { reply: await arrive(door, { id: h.id }, payload.method, args, true), ephemeralPk, heard: true };
|
|
76
97
|
};
|
|
77
98
|
|
|
78
99
|
// The door itself. Always answers bytes. When the ask did not open, the
|
|
@@ -82,13 +103,14 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
|
|
|
82
103
|
// A lid that is not a key -- a small-order point, of which the two curves
|
|
83
104
|
// have several each -- makes a dead agreement, and the seal refuses it. The
|
|
84
105
|
// door never throws: that reply is noise, sealed to a key nobody holds.
|
|
85
|
-
return async function door(bytes: Uint8Array): Promise<
|
|
106
|
+
return async function door(bytes: Uint8Array): Promise<Judged> {
|
|
86
107
|
const out = await judge(bytes);
|
|
87
108
|
const reply = out?.reply ?? SILENCE;
|
|
109
|
+
const heard = out?.heard ?? false;
|
|
88
110
|
try {
|
|
89
|
-
return await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32));
|
|
111
|
+
return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
|
|
90
112
|
} catch {
|
|
91
|
-
return sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32));
|
|
113
|
+
return { bytes: await sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
|
|
92
114
|
}
|
|
93
115
|
};
|
|
94
116
|
}
|
package/src/ward/ground.ts
CHANGED
|
@@ -28,8 +28,12 @@ export type Ground = {
|
|
|
28
28
|
random(n: number): Uint8Array; // entropy. every key a ward mints is drawn from it
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
// What a ward hands back. Two pointers.
|
|
31
|
+
// What a ward hands back. Two pointers. The door answers bytes, always, and
|
|
32
|
+
// one bit beside them: whether a key it holds spoke. That is all a harbor
|
|
33
|
+
// learns from an arrival, and it is what a harbor can act on -- a pk that
|
|
34
|
+
// only ever brings strangers' bytes is the harbor's to rate or refuse. Never
|
|
35
|
+
// a reason: the reason is sealed to the asker's lid, and is theirs.
|
|
32
36
|
export type WardPointers = {
|
|
33
|
-
door(bytes: Uint8Array): Promise<Uint8Array>;
|
|
37
|
+
door(bytes: Uint8Array): Promise<{ bytes: Uint8Array; heard: boolean }>;
|
|
34
38
|
ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
35
39
|
};
|
package/src/ward/heirs.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// speak, and so may the key it announced last time. A knock is the first
|
|
5
5
|
// use of the heir, which the inviter announced on your behalf, and the heir
|
|
6
6
|
// dies as it speaks.
|
|
7
|
-
import type {
|
|
7
|
+
import type { DoorWord } from '../being/types.ts';
|
|
8
|
+
import { GONE, type Heir, type Partition } from './partition.ts';
|
|
8
9
|
|
|
9
10
|
// How wide the span is, is the ward's own — wider is more forgiving of a
|
|
10
11
|
// rough road, and no peer can tell the difference except by being refused.
|
|
@@ -18,8 +19,22 @@ export class Heirs {
|
|
|
18
19
|
open(heir: string, being: string, id: string): void {
|
|
19
20
|
this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
|
|
20
21
|
}
|
|
22
|
+
// The id was removed. The heir is forgotten, and its last keys are kept
|
|
23
|
+
// apart, bounded, so that whoever still holds them hears `removed` at the
|
|
24
|
+
// door, once they have signed as themselves, and nobody else hears a thing.
|
|
21
25
|
close(heir: string): void {
|
|
26
|
+
const h = this.#p.heirs[heir];
|
|
27
|
+
if (!h) return;
|
|
22
28
|
delete this.#p.heirs[heir];
|
|
29
|
+
this.#p.gone[heir] = { current: h.current, announced: h.announced };
|
|
30
|
+
const keys = Object.keys(this.#p.gone);
|
|
31
|
+
for (const old of keys.slice(0, Math.max(0, keys.length - GONE))) delete this.#p.gone[old];
|
|
32
|
+
}
|
|
33
|
+
// May `by` speak for a relation she removed? True only for the keys the
|
|
34
|
+
// door held when the id went, which nobody but their holder has.
|
|
35
|
+
gone(heir: string, by: string): boolean {
|
|
36
|
+
const g = this.#p.gone[heir];
|
|
37
|
+
return !!g && (by === g.current || by === g.announced);
|
|
23
38
|
}
|
|
24
39
|
get(heir: string): Heir | undefined {
|
|
25
40
|
return this.#p.heirs[heir];
|
|
@@ -63,10 +78,10 @@ export class Heirs {
|
|
|
63
78
|
// neither: a call that binds nothing must not burn a number on its way to
|
|
64
79
|
// being refused, or a stranger who cannot be heard would still leave a mark
|
|
65
80
|
// behind her. Every write below this line is one that is going to hold.
|
|
66
|
-
honour(h: Heir, by: string, next: string | null, seq: number):
|
|
67
|
-
if (h.fresh && next === null) return
|
|
68
|
-
if (!this.spend(h, seq)) return
|
|
69
|
-
return this.settle(h, by, next);
|
|
81
|
+
honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord {
|
|
82
|
+
if (h.fresh && next === null) return 'unannounced'; // a knock without a key of her own binds nothing
|
|
83
|
+
if (!this.spend(h, seq)) return 'repeated';
|
|
84
|
+
return this.settle(h, by, next) ? true : 'unannounced';
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
// The signature checked out. Settle the keys: a fresh heir rotates at once
|
package/src/ward/owner.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// describe, boot, invite, knock, remove. The knock is a being's knock made
|
|
5
5
|
// for her, and the standing is written into her cells under the id the
|
|
6
6
|
// owner gave; remove takes a relation out of her by id, the mirror of it.
|
|
7
|
-
import { isSilence,
|
|
7
|
+
import { isSilence, isWord, wordOf } from '../being/silence.ts';
|
|
8
8
|
import { digest } from '../being/digest.ts';
|
|
9
9
|
import { OWNER } from '../being/types.ts';
|
|
10
10
|
import type { Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
|
|
@@ -78,7 +78,8 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
78
78
|
// The owner is a caller like any other and may say what this knock may
|
|
79
79
|
// spend. Saying nothing is the ward's default, as it is for a being.
|
|
80
80
|
const out = await door.stance.standings.knock(inv, args.method as string | undefined, (args.args as JsonObject) ?? {}, args.wanted as Wanted | undefined);
|
|
81
|
-
if (isSilence(out)
|
|
81
|
+
if (isSilence(out)) return { error: 'silence' };
|
|
82
|
+
if (isWord(out)) return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
|
|
82
83
|
return { taken: await door.stance.standings.take(id, inv), answer: out as Json };
|
|
83
84
|
}
|
|
84
85
|
if (method === 'remove') {
|
|
@@ -109,7 +110,7 @@ async function describe(w: OwnerSide): Promise<Json> {
|
|
|
109
110
|
let d: string | null = null;
|
|
110
111
|
try {
|
|
111
112
|
const bp = await (door.being.answer as (a: unknown) => unknown)({ id: OWNER });
|
|
112
|
-
d = isSilence(bp) ||
|
|
113
|
+
d = isSilence(bp) || isWord(bp) ? null : await digest(bp as Json);
|
|
113
114
|
} catch {
|
|
114
115
|
// she threw where she was asked. there is no blueprint, and d stays null.
|
|
115
116
|
}
|
package/src/ward/partition.ts
CHANGED
|
@@ -78,11 +78,20 @@ export type Bind = {
|
|
|
78
78
|
minted: string[]; // the last eight pks her side minted. inner. a test reads it
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// A relation she removed, as the door remembers it: the keys that may still
|
|
82
|
+
// speak for it, so their holder hears `removed` and a stranger still hears
|
|
83
|
+
// silence. Nothing else survives the removal. The list is bounded, oldest
|
|
84
|
+
// out, because a door that remembered every relation ever ended would grow
|
|
85
|
+
// for as long as she keeps inviting.
|
|
86
|
+
export type Gone = { current: string; announced: string | null };
|
|
87
|
+
export const GONE = 256;
|
|
88
|
+
|
|
81
89
|
export type Partition = {
|
|
82
90
|
version: string;
|
|
83
91
|
beings: Record<string, Cells & { class?: string }>; // key -> her cells
|
|
84
92
|
bind: Record<string, Bind>; // key -> her bind table
|
|
85
93
|
heirs: Record<string, Heir>; // heir pk -> occupant, across every being of the ward
|
|
94
|
+
gone: Record<string, Gone>; // heir pk -> the last keys of a relation she removed
|
|
86
95
|
public: string | null; // the one public being's key
|
|
87
96
|
};
|
|
88
97
|
|
|
@@ -101,6 +110,7 @@ export function open(memory: Record<string, unknown>): Partition {
|
|
|
101
110
|
p.beings ??= {};
|
|
102
111
|
p.bind ??= {};
|
|
103
112
|
p.heirs ??= {};
|
|
113
|
+
p.gone ??= {};
|
|
104
114
|
p.public ??= null;
|
|
105
115
|
return p;
|
|
106
116
|
}
|
package/src/ward/seal.ts
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
// itself. The door opens every ask with its own padlock and reads `to` after,
|
|
19
19
|
// so nothing was ever gained by having it in the clear.
|
|
20
20
|
// reply on the wire box = eph X pk || AESGCM( reply || signature ) sealed to the ask's eph pk
|
|
21
|
-
// reply = JSON { object, seen } | { silence: true } signed by the ward key
|
|
22
|
-
import type { Json, JsonObject } from '../being/types.ts';
|
|
21
|
+
// reply = JSON { object, seen } | { silence: true } | { quo: word } signed by the ward key
|
|
22
|
+
import type { DoorWord, Json, JsonObject } from '../being/types.ts';
|
|
23
|
+
import { isDoorWord } from '../being/silence.ts';
|
|
23
24
|
import { KEY, SIGNATURE, box, concat, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.ts';
|
|
24
25
|
|
|
25
26
|
const utf8 = new TextEncoder();
|
|
@@ -58,7 +59,7 @@ export type AskPayload = { by: string; next: string | null; seq: number; time: n
|
|
|
58
59
|
// What is actually sealed: the payload with the heir it is for, or null for
|
|
59
60
|
// the public being. `to` is signed with the rest, so it cannot be moved.
|
|
60
61
|
type Sealed = AskPayload & { to: string | null };
|
|
61
|
-
export type ReplyPayload = { object: Json; seen: string | null } | { silence: true };
|
|
62
|
+
export type ReplyPayload = { object: Json; seen: string | null } | { silence: true } | { quo: DoorWord };
|
|
62
63
|
|
|
63
64
|
// Seal an ask. Returns the bytes and the ephemeral secret the reply will be sealed to.
|
|
64
65
|
export async function sealAsk(to: string | null, payload: AskPayload, signer: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{ bytes: Uint8Array; ephemeral: Uint8Array }> {
|
|
@@ -109,8 +110,9 @@ export async function sealReply(reply: ReplyPayload, ephemeralPk: Uint8Array, wa
|
|
|
109
110
|
// Open a reply with the ephemeral secret kept from the ask, and verify it
|
|
110
111
|
// came from the ward it was sent to. Null is silence: it did not open, it
|
|
111
112
|
// lied, or it is not a reply. Every ward on the wire is somebody else's code,
|
|
112
|
-
// so the shape is checked here and nowhere later: a reply is `{ silence }
|
|
113
|
-
//
|
|
113
|
+
// so the shape is checked here and nowhere later: a reply is `{ silence }`,
|
|
114
|
+
// `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
|
|
115
|
+
// object present and seen a digest or null.
|
|
114
116
|
// Anything else is bytes that said nothing, and her ward never hands her a
|
|
115
117
|
// throw or an absent value for what a stranger wrote.
|
|
116
118
|
export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, signPk: Uint8Array): Promise<ReplyPayload | null> {
|
|
@@ -124,6 +126,7 @@ export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, sig
|
|
|
124
126
|
if (reply === null || typeof reply !== 'object' || Array.isArray(reply)) return null;
|
|
125
127
|
const r = reply as Record<string, unknown>;
|
|
126
128
|
if (r.silence === true) return { silence: true };
|
|
129
|
+
if (Object.hasOwn(r, 'quo')) return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
127
130
|
if (!Object.hasOwn(r, 'object') || r.object === undefined) return null;
|
|
128
131
|
if (r.seen !== null && !(typeof r.seen === 'string' && /^[0-9a-f]{64}$/.test(r.seen))) return null;
|
|
129
132
|
return { object: r.object as Json, seen: r.seen };
|
package/src/ward/stance.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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.ts';
|
|
4
|
+
import { silence, isSilence, isUnreached, isWord, word } from '../being/silence.ts';
|
|
5
5
|
import { digest } from '../being/digest.ts';
|
|
6
6
|
import { RESERVED_IDS } from '../being/types.ts';
|
|
7
|
-
import type { Answer, Cells, Invitation, JsonObject, Silence, Stance, Standing, Standings,
|
|
7
|
+
import type { Answer, Cells, Invitation, JsonObject, Silence, Stance, Standing, Standings, Wanted, Word } from '../being/types.ts';
|
|
8
8
|
import { at, put, drop, type Bind, type StandingKeys } from './partition.ts';
|
|
9
9
|
import { allow, within, LATE } from './allowance.ts';
|
|
10
10
|
import { isWardPk, type ReplyPayload } from './seal.ts';
|
|
@@ -17,7 +17,7 @@ export type Inside = {
|
|
|
17
17
|
closeHeir(heir: string): void;
|
|
18
18
|
// one send for every destination. the far door's reply, silence, or unreached.
|
|
19
19
|
// signs as keys.current, announces keys.next (minting one if asked), and rotates keys on an answer.
|
|
20
|
-
send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence |
|
|
20
|
+
send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
// An invitation as it travels: the far ward's pk, the heir pk, the heir's secret seed. Or just the ward, for its public being.
|
|
@@ -84,13 +84,14 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
84
84
|
const out = await within(allow(wanted).time, work(() => !late), () => {
|
|
85
85
|
late = true;
|
|
86
86
|
});
|
|
87
|
-
return out === LATE ?
|
|
87
|
+
return out === LATE ? word('late') : out;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
const send = async (keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, rec?: Cells['standings'][string]): Promise<Answer> => {
|
|
91
91
|
const r = await inside.send(bind, keys, true, method, args, wanted);
|
|
92
|
-
if (isSilence(r) ||
|
|
92
|
+
if (isSilence(r) || isWord(r)) return r;
|
|
93
93
|
if ('silence' in r) return silence;
|
|
94
|
+
if ('quo' in r) return word(r.quo); // the far door's word for her, sealed to her lid
|
|
94
95
|
if (rec && method !== undefined && r.seen !== null) rec.seen = r.seen; // the digest rode along
|
|
95
96
|
return r.object;
|
|
96
97
|
};
|
|
@@ -122,7 +123,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
122
123
|
k.current = own.current;
|
|
123
124
|
k.next = own.next;
|
|
124
125
|
if (isUnreached(r)) return r; // still nothing. she knows no more than before.
|
|
125
|
-
if (!isSilence(r) && !('
|
|
126
|
+
if (!isSilence(r) && !isWord(r) && 'object' in r) {
|
|
126
127
|
k.spoke = true; // it was heard the first time, and this is the answer
|
|
127
128
|
bind.answered[name] = true;
|
|
128
129
|
return r.object;
|
|
@@ -144,8 +145,13 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
144
145
|
k.next = keys.next;
|
|
145
146
|
}
|
|
146
147
|
if (isUnreached(r)) return r; // nothing came back. she does not know whether it was heard.
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
// The heir is spent once the door honoured it, whatever she then said: an
|
|
149
|
+
// object, silence, or that she threw. A refusal wrote nothing, and says
|
|
150
|
+
// so by its word, so the heir still speaks and the next knock is as it.
|
|
151
|
+
k.spoke = !(!isSilence(r) && !isWord(r) && 'quo' in r && r.quo !== 'threw');
|
|
152
|
+
if (isSilence(r) || isWord(r)) return r;
|
|
153
|
+
if ('silence' in r) return silence;
|
|
154
|
+
if ('quo' in r) return word(r.quo);
|
|
149
155
|
bind.answered[name] = true;
|
|
150
156
|
return r.object;
|
|
151
157
|
};
|
|
@@ -158,14 +164,14 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
158
164
|
|
|
159
165
|
const calls = {
|
|
160
166
|
knock: async (inv: Invitation, method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
|
|
161
|
-
if (!valid(inv)) return
|
|
167
|
+
if (!valid(inv)) return word('invitation'); // S1. nothing is sent
|
|
162
168
|
// She may knock again, and after take that knock is an ask: the relation
|
|
163
169
|
// has one home, so it answers on the standing's lane, under the
|
|
164
170
|
// standing's keys and its count. A second lane here would speak for the
|
|
165
171
|
// same relation, and the two would refuse each other.
|
|
166
172
|
const id = takenAs(nameOf(inv));
|
|
167
|
-
if (id !== undefined) return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, bind.standings[id], cells.standings[id], method, args, wanted) : Promise.resolve(
|
|
168
|
-
return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(
|
|
173
|
+
if (id !== undefined) return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, bind.standings[id], cells.standings[id], method, args, wanted) : Promise.resolve(word('late')))));
|
|
174
|
+
return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
|
|
169
175
|
},
|
|
170
176
|
// Take consumes. Until now the relation lived in the knock record, under
|
|
171
177
|
// the ward and the heir; from now it lives in the standing, under her id.
|
|
@@ -214,15 +220,15 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
214
220
|
// the next line. Waiting for the lane is not a reason to lose it.
|
|
215
221
|
ask: (method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
|
|
216
222
|
const keys = at(bind.standings, id);
|
|
217
|
-
if (!keys) return Promise.resolve(
|
|
223
|
+
if (!keys) return Promise.resolve(word('dropped') as Answer); // S2. she dropped it between one line and the next
|
|
218
224
|
const rec = at(cells.standings, id);
|
|
219
|
-
return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(
|
|
225
|
+
return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(word('late')))));
|
|
220
226
|
},
|
|
221
227
|
});
|
|
222
228
|
|
|
223
229
|
const asking = async (id: string, keys: StandingKeys, rec: Cells['standings'][string] | undefined, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
|
|
224
230
|
const out = await send(keys, method, args, wanted, rec);
|
|
225
|
-
if (method === undefined && !isSilence(out) && !
|
|
231
|
+
if (method === undefined && !isSilence(out) && !isWord(out)) {
|
|
226
232
|
const here = at(cells.standings, id);
|
|
227
233
|
if (here) {
|
|
228
234
|
here.blueprint = out as never;
|
package/src/ward/ward.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// the first being in its own map, and every being it later instantiates gets
|
|
5
5
|
// a stance built by the same code. Mints every key, seals every ask that
|
|
6
6
|
// leaves, opens every one that arrives.
|
|
7
|
-
import { silence, isSilence, unreached,
|
|
7
|
+
import { silence, isSilence, unreached, isWord, word } from '../being/silence.ts';
|
|
8
8
|
import { OWNER } from '../being/types.ts';
|
|
9
|
-
import type { Asker, BeingLike, Cells, JsonObject, Reply, Stance, Wanted } from '../being/types.ts';
|
|
9
|
+
import type { Asker, BeingLike, Cells, JsonObject, Reply, Stance, Wanted, Word } from '../being/types.ts';
|
|
10
10
|
import type { Ground, WardPointers } from './ground.ts';
|
|
11
11
|
import { at, open, put, emptyBind, emptyCells, type Bind, type Partition, type StandingKeys } from './partition.ts';
|
|
12
12
|
import { Heirs } from './heirs.ts';
|
|
@@ -72,7 +72,7 @@ class Self implements BeingLike {
|
|
|
72
72
|
this.ask = async (method, args = {}) => {
|
|
73
73
|
try {
|
|
74
74
|
const out = await this.answer({ id: OWNER }, method, args as JsonObject);
|
|
75
|
-
return isSilence(out) ||
|
|
75
|
+
return isSilence(out) || isWord(out) ? silence : out;
|
|
76
76
|
} catch {
|
|
77
77
|
return silence;
|
|
78
78
|
}
|
|
@@ -151,7 +151,7 @@ class Self implements BeingLike {
|
|
|
151
151
|
// next, seals to the far ward, opens the reply with the ephemeral secret,
|
|
152
152
|
// and rotates to the announced key once the far door has answered under
|
|
153
153
|
// the current one.
|
|
154
|
-
async #send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted?: Wanted): Promise<ReplyPayload | typeof silence |
|
|
154
|
+
async #send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted?: Wanted): Promise<ReplyPayload | typeof silence | Word> {
|
|
155
155
|
// What she asked for, held to what this ward allows. Asking for nothing is
|
|
156
156
|
// the default, and asking for more than the ceiling is the ceiling: budget
|
|
157
157
|
// is granted by a ward, never minted by a being.
|
|
@@ -185,22 +185,22 @@ class Self implements BeingLike {
|
|
|
185
185
|
// relation that comes back round holds a lane the answer needs, and only a
|
|
186
186
|
// bound on the wait can break that. What comes back late is not read.
|
|
187
187
|
//
|
|
188
|
-
// A wait that ran out is
|
|
188
|
+
// A wait that ran out is `late`, never unreached. Unreached promises
|
|
189
189
|
// nothing was delivered and is safe to retry; a bound that expired knows
|
|
190
190
|
// no such thing, because the far door may have heard and be working still.
|
|
191
191
|
//
|
|
192
192
|
// A harbor answers bytes or nothing. One that throws instead has answered
|
|
193
193
|
// nothing in a louder voice, and is read as nothing: no door was reached.
|
|
194
|
-
const carried = (keys.ward === this.pk ? this.door(new Uint8Array(bytes)) : this.g.carry(keys.ward, bytes)).then(
|
|
194
|
+
const carried = (keys.ward === this.pk ? this.door(new Uint8Array(bytes)).then((r) => r.bytes) : this.g.carry(keys.ward, bytes)).then(
|
|
195
195
|
(b) => b,
|
|
196
196
|
() => undefined,
|
|
197
197
|
);
|
|
198
198
|
const out = await within(budget.time, carried);
|
|
199
|
-
if (out === LATE) return
|
|
199
|
+
if (out === LATE) return word('late');
|
|
200
200
|
if (out === undefined) return unreached(); // nothing came back. no door was reached.
|
|
201
201
|
const reply = await openReply(out, ephemeral, wardSignPk(keys.ward));
|
|
202
|
-
if (!reply) return silence; // did not open, or not from that ward
|
|
203
|
-
if (
|
|
202
|
+
if (!reply) return silence; // did not open, or not from that ward: not Quo's bytes
|
|
203
|
+
if ('object' in reply && keys.next !== null) {
|
|
204
204
|
keys.current = keys.next; // the far door holds `next` as announced. move to it.
|
|
205
205
|
keys.next = null;
|
|
206
206
|
}
|
package/vectors/framing.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"corpus": "@quo-systems/quo",
|
|
3
3
|
"encoding": "hex",
|
|
4
4
|
"area": "framing",
|
|
5
|
-
"note": "The formats a second kit must reproduce byte for byte: the ward pk, the digest, the signed ask body, the sealed ask, the sealed reply, the invitation and the knock. Every random input is fixed here, so every output is fixed. The ask body is JSON.stringify of the payload, which is not canonical: these vectors pin the key order sealAsk writes, to, by, next, seq, time, method, args; hops is in no vector, because nothing sets it. A kit that emits another order still interoperates, because a door verifies the bytes it received, but it will not reproduce these vectors. An invitation is JSON too, ward, heir, secret, and a heir secret is the seed of a being key. A knock is an ask whose by is the heir pk, signed with the heir secret, whose next is the knocker's own key, and whose to is the heir; its answer is an ordinary reply, and the heir is spent by it. The ward here is the one from the ward pk vector, and the heir secret is the first entropy it draws, so the invitation it mints and the knock its door binds are these bytes.",
|
|
5
|
+
"note": "The formats a second kit must reproduce byte for byte: the ward pk, the digest, the signed ask body, the sealed ask, the sealed reply, the invitation and the knock. Every random input is fixed here, so every output is fixed. The ask body is JSON.stringify of the payload, which is not canonical: these vectors pin the key order sealAsk writes, to, by, next, seq, time, method, args; hops is in no vector, because nothing sets it. A kit that emits another order still interoperates, because a door verifies the bytes it received, but it will not reproduce these vectors. An invitation is JSON too, ward, heir, secret, and a heir secret is the seed of a being key. A knock is an ask whose by is the heir pk, signed with the heir secret, whose next is the knocker's own key, and whose to is the heir; its answer is an ordinary reply, and the heir is spent by it. A reply is one of three shapes: an object with its digest, silence, or the ward's word to a key the door has bound, { quo: word }, where word is one of removed, absent, unannounced, repeated, threw. The ward here is the one from the ward pk vector, and the heir secret is the first entropy it draws, so the invitation it mints and the knock its door binds are these bytes.",
|
|
6
6
|
"vectors": [
|
|
7
7
|
{
|
|
8
8
|
"name": "a ward pk from a fixed seed",
|
|
@@ -53,6 +53,12 @@
|
|
|
53
53
|
"replySeed": "4444444444444444444444444444444444444444444444444444444444444444",
|
|
54
54
|
"bytes": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b76ec93e97a657e002d9135a7394f3ec33bcd723c9d8947909f4cc659d8dfeed743f5b64bfa87b4ac79074219c1032037de444400b509f31559331b7995f9dfbadbe7fb0f6d46db2257db70f248b7739b3baa2ca79b07b66b29ea680faf79340f2309163c9e3c65fcdb33297c419d1cd8517647"
|
|
55
55
|
},
|
|
56
|
+
{
|
|
57
|
+
"name": "a sealed word reply: the ward's word to a key it has bound",
|
|
58
|
+
"reply": "{\"quo\":\"removed\"}",
|
|
59
|
+
"replySeed": "4444444444444444444444444444444444444444444444444444444444444444",
|
|
60
|
+
"bytes": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b76ec8dfe7f2227567dce23ea274378db64eb74efed5c91135d62d89db62db7d8ffbd54cf3e82e3272d0b26d59130020755b442a16340798d3eec60bc3a3875020da9276d5e72d712ac3dad7ef287d3c153a0588bf4951b9d3540b1142bd54875a3"
|
|
61
|
+
},
|
|
56
62
|
{
|
|
57
63
|
"name": "an invitation for a heir: the ward pk, the heir pk, the heir secret, and nothing else",
|
|
58
64
|
"wardSeed": "1111111111111111111111111111111111111111111111111111111111111111",
|