@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.
Files changed (46) hide show
  1. package/SPEC.md +185 -114
  2. package/dist/being/index.d.ts +1 -1
  3. package/dist/being/index.js +1 -1
  4. package/dist/being/silence.d.ts +6 -1
  5. package/dist/being/silence.js +25 -7
  6. package/dist/being/types.d.ts +8 -4
  7. package/dist/being/types.js +1 -1
  8. package/dist/conformance/beings.d.ts +1 -1
  9. package/dist/conformance/beings.js +5 -5
  10. package/dist/conformance/estate.js +12 -6
  11. package/dist/conformance/index.js +14 -12
  12. package/dist/harbor/core.d.ts +6 -1
  13. package/dist/harbor/core.js +11 -2
  14. package/dist/harbor/memory.d.ts +5 -1
  15. package/dist/harbor/memory.js +7 -2
  16. package/dist/ward/door.d.ts +6 -2
  17. package/dist/ward/door.js +55 -35
  18. package/dist/ward/ground.d.ts +4 -1
  19. package/dist/ward/heirs.d.ts +4 -2
  20. package/dist/ward/heirs.js +20 -3
  21. package/dist/ward/owner.js +6 -4
  22. package/dist/ward/partition.d.ts +6 -0
  23. package/dist/ward/partition.js +2 -0
  24. package/dist/ward/seal.d.ts +3 -1
  25. package/dist/ward/seal.js +6 -2
  26. package/dist/ward/stance.d.ts +2 -2
  27. package/dist/ward/stance.js +21 -12
  28. package/dist/ward/ward.js +7 -7
  29. package/package.json +19 -31
  30. package/src/being/index.ts +1 -1
  31. package/src/being/silence.ts +26 -8
  32. package/src/being/types.ts +13 -3
  33. package/src/conformance/beings.ts +5 -5
  34. package/src/conformance/estate.ts +11 -6
  35. package/src/conformance/index.ts +14 -12
  36. package/src/harbor/core.ts +11 -2
  37. package/src/harbor/memory.ts +6 -1
  38. package/src/ward/door.ts +54 -32
  39. package/src/ward/ground.ts +6 -2
  40. package/src/ward/heirs.ts +20 -5
  41. package/src/ward/owner.ts +4 -3
  42. package/src/ward/partition.ts +10 -0
  43. package/src/ward/seal.ts +8 -5
  44. package/src/ward/stance.ts +20 -14
  45. package/src/ward/ward.ts +9 -9
  46. 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. Every failure is
3
- // one silence, sealed to whoever asked so that a stranger learns nothing,
4
- // not even which case they hit. What passes is named and dispatched to her
5
- // answer, and her digest for that asker rides back with the object.
6
- import { isSilence, isUnreached } from '../being/silence.ts';
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
- export async function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject): Promise<ReplyPayload> {
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 SILENCE; // she threw where she was asked. there is no answer.
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) || isUnreached(out)) return SILENCE;
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) || isUnreached(bp) ? null : await digest(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<{ reply: ReplyPayload; ephemeralPk: Uint8Array } | null> => {
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
- // The allowance, read before anything is done under it. A budget already
51
- // gone is one silence like every other refusal here: a stranger learns
52
- // nothing, not even that what she ran out of was time. Nothing is spent
53
- // and nothing rotates, because nothing was heard.
54
- if (spent({ time: payload.time })) return { reply: SILENCE, ephemeralPk };
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 { reply: SILENCE, ephemeralPk };
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) return { reply: SILENCE, ephemeralPk };
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 || !at(door.cells.occupants, h.id)) return { reply: SILENCE, ephemeralPk }; // she removed it, or she is gone
70
- if (!(await verifyAsk(a, payload.by))) return { reply: SILENCE, ephemeralPk };
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
- if (!heirs.honour(h, payload.by, payload.next, payload.seq)) return { reply: SILENCE, ephemeralPk };
75
- return { reply: await arrive(door, { id: h.id }, payload.method, args), ephemeralPk };
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<Uint8Array> {
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
  }
@@ -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 { Heir, Partition } from './partition.ts';
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): boolean {
67
- if (h.fresh && next === null) return false; // a knock without a key of her own binds nothing
68
- if (!this.spend(h, seq)) return false;
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, isUnreached } from '../being/silence.ts';
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) || isUnreached(out)) return { error: isUnreached(out) ? 'unreached' : 'silence' };
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) || isUnreached(bp) ? null : await digest(bp as Json);
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
  }
@@ -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
- // or `{ object, seen }` with an object present and seen a digest or null.
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 };
@@ -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, Unreached, Wanted } from '../being/types.ts';
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 | Unreached>;
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 ? silence : out;
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) || isUnreached(r)) return 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) && !('silence' in 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
- k.spoke = true;
148
- if (isSilence(r) || 'silence' in r) return silence;
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 silence;
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(silence))));
168
- return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(silence))));
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(silence as Answer); // she dropped it between one line and the next
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(silence))));
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) && !isUnreached(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, isUnreached } from '../being/silence.ts';
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) || isUnreached(out) ? silence : 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 | ReturnType<typeof unreached>> {
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 silence, never unreached. Unreached promises
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 silence;
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 (!('silence' in reply) && keys.next !== null) {
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
  }
@@ -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",