@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.
Files changed (49) hide show
  1. package/SPEC.md +259 -129
  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 +23 -34
  11. package/dist/conformance/index.js +14 -12
  12. package/dist/harbor/core.d.ts +7 -1
  13. package/dist/harbor/core.js +25 -2
  14. package/dist/harbor/dial.js +7 -1
  15. package/dist/harbor/memory.d.ts +5 -1
  16. package/dist/harbor/memory.js +7 -2
  17. package/dist/ward/door.d.ts +6 -2
  18. package/dist/ward/door.js +55 -35
  19. package/dist/ward/ground.d.ts +4 -1
  20. package/dist/ward/heirs.d.ts +4 -2
  21. package/dist/ward/heirs.js +20 -3
  22. package/dist/ward/owner.d.ts +5 -3
  23. package/dist/ward/owner.js +55 -16
  24. package/dist/ward/partition.d.ts +6 -0
  25. package/dist/ward/partition.js +2 -0
  26. package/dist/ward/seal.d.ts +3 -1
  27. package/dist/ward/seal.js +6 -2
  28. package/dist/ward/stance.d.ts +2 -2
  29. package/dist/ward/stance.js +21 -12
  30. package/dist/ward/ward.js +37 -12
  31. package/package.json +19 -31
  32. package/src/being/index.ts +1 -1
  33. package/src/being/silence.ts +26 -8
  34. package/src/being/types.ts +13 -3
  35. package/src/conformance/beings.ts +5 -5
  36. package/src/conformance/estate.ts +22 -36
  37. package/src/conformance/index.ts +14 -12
  38. package/src/harbor/core.ts +24 -2
  39. package/src/harbor/dial.ts +7 -1
  40. package/src/harbor/memory.ts +6 -1
  41. package/src/ward/door.ts +54 -32
  42. package/src/ward/ground.ts +6 -2
  43. package/src/ward/heirs.ts +20 -5
  44. package/src/ward/owner.ts +54 -16
  45. package/src/ward/partition.ts +10 -0
  46. package/src/ward/seal.ts +8 -5
  47. package/src/ward/stance.ts +20 -14
  48. package/src/ward/ward.ts +37 -13
  49. 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. 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.js';
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
- // One arrival at one being, already named. Catches every throw.
14
- export async function arrive(door, asker, method, args) {
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 SILENCE; // she threw where she was asked. there is no answer.
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) || isUnreached(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) || isUnreached(bp) ? null : await digest(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
- // The allowance, read before anything is done under it. A budget already
49
- // gone is one silence like every other refusal here: a stranger learns
50
- // nothing, not even that what she ran out of was time. Nothing is spent
51
- // and nothing rotates, because nothing was heard.
52
- if (spent({ time: payload.time }))
53
- return { reply: SILENCE, ephemeralPk };
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 { reply: SILENCE, ephemeralPk };
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
- return { reply: SILENCE, ephemeralPk };
70
- const door = doors.get(h.being);
71
- if (!door || !at(door.cells.occupants, h.id))
72
- return { reply: SILENCE, ephemeralPk }; // she removed it, or she is gone
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 { reply: SILENCE, ephemeralPk };
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
- if (!heirs.honour(h, payload.by, payload.next, payload.seq))
79
- return { reply: SILENCE, ephemeralPk };
80
- return { reply: await arrive(door, { id: h.id }, payload.method, args), ephemeralPk };
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
  }
@@ -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<Uint8Array>;
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
  };
@@ -1,12 +1,14 @@
1
- import type { Heir, Partition } from './partition.ts';
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): boolean;
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
  }
@@ -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 false; // a knock without a key of her own binds nothing
80
+ return 'unannounced'; // a knock without a key of her own binds nothing
64
81
  if (!this.spend(h, seq))
65
- return false;
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
@@ -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, isPublic?: boolean): {
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: readonly ['boot', 'invite', 'knock', 'remove'];
33
+ export declare const OWNER_ASKS: Ask[];
32
34
  export declare function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json>;
@@ -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. Five asks:
4
- // describe, boot, invite, knock, remove. The knock is a being's knock made
5
- // for her, and the standing is written into her cells under the id the
6
- // owner gave; remove takes a relation out of her by id, the mirror of it.
7
- import { isSilence, isUnreached } from '../being/silence.js';
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
- export const OWNER_ASKS = ['boot', 'invite', 'knock', 'remove'];
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, args.public === true);
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) || isUnreached(out))
67
- return { error: isUnreached(out) ? 'unreached' : 'silence' };
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) || isUnreached(bp) ? null : await digest(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.map((name) => ({ name, input: { type: 'object' } })),
150
+ asks: OWNER_ASKS,
112
151
  notes: { pk: w.pk, beings },
113
152
  };
114
153
  }
@@ -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;
@@ -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
  }
@@ -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
- // or `{ object, seen }` with an object present and seen a digest or null.
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)))
@@ -1,4 +1,4 @@
1
- import type { Cells, JsonObject, Silence, Stance, Unreached, Wanted } from '../being/types.ts';
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 | Unreached>;
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;
@@ -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 ? silence : out;
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) || isUnreached(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) && !('silence' in 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
- k.spoke = true;
131
- if (isSilence(r) || 'silence' in r)
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 silence;
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(silence))));
152
- return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(silence))));
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(silence); // she dropped it between one line and the next
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(silence))));
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) && !isUnreached(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;