@quo-systems/quo 0.2.8 → 0.2.10

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 (52) hide show
  1. package/README.md +19 -10
  2. package/SPEC.md +161 -89
  3. package/dist/being/being.d.ts +2 -2
  4. package/dist/being/being.js +27 -11
  5. package/dist/being/digest.js +11 -7
  6. package/dist/being/types.d.ts +2 -2
  7. package/dist/conformance/estate.js +1 -3
  8. package/dist/conformance/index.js +1 -1
  9. package/dist/harbor/core.d.ts +2 -0
  10. package/dist/harbor/core.js +114 -25
  11. package/dist/harbor/dial.js +5 -3
  12. package/dist/harbor/memory.d.ts +1 -1
  13. package/dist/harbor/memory.js +3 -2
  14. package/dist/harbor/reach.js +32 -7
  15. package/dist/ward/allowance.js +8 -2
  16. package/dist/ward/arithmetic.js +11 -3
  17. package/dist/ward/cells.d.ts +3 -1
  18. package/dist/ward/cells.js +79 -21
  19. package/dist/ward/door.d.ts +1 -0
  20. package/dist/ward/door.js +24 -5
  21. package/dist/ward/ground.d.ts +1 -0
  22. package/dist/ward/heirs.d.ts +1 -1
  23. package/dist/ward/heirs.js +15 -8
  24. package/dist/ward/owner.d.ts +5 -26
  25. package/dist/ward/owner.js +24 -15
  26. package/dist/ward/seal.js +3 -0
  27. package/dist/ward/stance.d.ts +4 -1
  28. package/dist/ward/stance.js +116 -54
  29. package/dist/ward/ward.d.ts +10 -0
  30. package/dist/ward/ward.js +87 -36
  31. package/package.json +4 -2
  32. package/src/being/being.ts +26 -10
  33. package/src/being/digest.ts +13 -11
  34. package/src/being/types.ts +10 -4
  35. package/src/conformance/estate.ts +1 -3
  36. package/src/conformance/index.ts +1 -1
  37. package/src/harbor/core.ts +110 -26
  38. package/src/harbor/dial.ts +6 -4
  39. package/src/harbor/memory.ts +3 -2
  40. package/src/harbor/reach.ts +38 -11
  41. package/src/ward/allowance.ts +8 -2
  42. package/src/ward/arithmetic.ts +11 -3
  43. package/src/ward/cells.ts +76 -25
  44. package/src/ward/door.ts +22 -5
  45. package/src/ward/ground.ts +9 -2
  46. package/src/ward/heirs.ts +13 -6
  47. package/src/ward/owner.ts +24 -24
  48. package/src/ward/seal.ts +2 -0
  49. package/src/ward/stance.ts +117 -48
  50. package/src/ward/ward.ts +86 -35
  51. package/vectors/arithmetic.json +7 -0
  52. package/vectors/framing.json +14 -5
@@ -1,13 +1,14 @@
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, isWord, word } from '../being/silence.ts';
4
+ import { silence, isSilence, isWord, word, wordOf } from '../being/silence.ts';
5
5
  import { digest } from '../being/digest.ts';
6
6
  import { RESERVED_IDS } from '../being/types.ts';
7
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';
11
+ import { cellFault } from './cells.ts';
11
12
 
12
13
  // What the stance needs from its ward, and no more.
13
14
  export type Inside = {
@@ -20,6 +21,12 @@ export type Inside = {
20
21
  send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
21
22
  // a new being of this ward, as the owner's boot makes one. the key, or null.
22
23
  instantiate(key: string, className: string): string | null;
24
+ // mint an occupant on a being of this ward, for the being who made her.
25
+ relate(key: string, id: string): Promise<Invitation | null>;
26
+ // take a being of this ward out again, with every relation she holds.
27
+ unmake(key: string): void;
28
+ // the bind table was written: a record put, dropped or moved on.
29
+ wrote(): void;
23
30
  };
24
31
 
25
32
  // 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.
@@ -72,9 +79,9 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
72
79
  // Every ask a being makes, bounded. The bound is outside the lane on
73
80
  // purpose: an ask that waits its turn behind a relation that comes back
74
81
  // round never reaches a send, and a bound that only watched the wire would
75
- // never see it hang. A bound that ran out is silence, never unreached:
76
- // unreached promises nothing was delivered and is safe to ask again, and a
77
- // wait that gave up knows no such thing.
82
+ // never see it hang. A bound that ran out is the word late, never
83
+ // unreached: unreached promises nothing was delivered and is safe to ask
84
+ // again, and a wait that gave up knows no such thing.
78
85
  //
79
86
  // The bell rings before the work is reached, too: an ask that was still
80
87
  // waiting its turn when the wait ran out is never sent. Sent then, it would
@@ -89,53 +96,89 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
89
96
  return out === LATE ? word('late') : out;
90
97
  };
91
98
 
99
+ // What one send came back as, read in one place for every path that sends.
100
+ // A reply that opened and was signed by the far ward is the door speaking,
101
+ // whatever it said: an object, its silence, or a word. Bytes that were not
102
+ // Quo's, late and unreached are not the door speaking, and say nothing
103
+ // about whether it heard.
104
+ type Read = { answer: Answer; door: boolean; seen: string | null };
105
+ const read = (r: ReplyPayload | Silence | Word): Read => {
106
+ if (isSilence(r) || isWord(r)) return { answer: r, door: false, seen: null };
107
+ if ('silence' in r) return { answer: silence, door: true, seen: null };
108
+ if ('quo' in r) return { answer: word(r.quo), door: true, seen: null }; // the far door's word for her, sealed to her lid
109
+ return { answer: r.object, door: true, seen: r.seen };
110
+ };
111
+ // Whether the door spent the heir this send spoke under. An object, the
112
+ // door's silence and threw all mean it was honoured. A refusal word wrote
113
+ // nothing and says so. Anything that is not the door speaking says nothing.
114
+ const honoured = (r: Read): boolean => r.door && !(isWord(r.answer) && wordOf(r.answer) !== 'threw');
115
+ const answered = (r: Read): boolean => r.door && !isSilence(r.answer) && !isWord(r.answer);
116
+
92
117
  const send = async (keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, rec?: Cells['standings'][string]): Promise<Answer> => {
93
- const r = await inside.send(bind, keys, true, method, args, wanted);
94
- if (isSilence(r) || isWord(r)) return r;
95
- if ('silence' in r) return silence;
96
- if ('quo' in r) return word(r.quo); // the far door's word for her, sealed to her lid
118
+ const r = read(await inside.send(bind, keys, true, method, args, wanted));
97
119
  if (rec && method !== undefined && r.seen !== null) rec.seen = r.seen; // the digest rode along
98
- return r.object;
120
+ return r.answer;
99
121
  };
100
122
 
101
123
  const knocking = async (inv: SealedInvitation, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
102
124
  const name = nameOf(inv);
125
+ // Taken while this knock waited its turn: the relation lives in the
126
+ // standing now and speaks on its lane, under its keys and its count.
127
+ const taken = takenAs(name);
128
+ if (taken !== undefined) {
129
+ const keys = at(bind.standings, taken);
130
+ if (!keys) return word('dropped');
131
+ return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted));
132
+ }
103
133
  // her own key for this relation, minted once: rotation two. reused if she knocks again.
104
134
  const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
105
- // the first knock on a heir speaks as the heir, the key the inviter handed out, and announces her own.
106
- // once bytes have come back, the heir is spent whatever they said, and her own key speaks.
107
- // She sent once as the heir and nothing came back. Either the door heard,
108
- // spent the heir and rotated to the key she announced, or nothing arrived
109
- // and the heir still stands. Only the far door knows, and it will not say.
135
+ // The first knock on a heir speaks as the heir, the key the inviter
136
+ // handed out, and announces her own. Once the door has honoured it the
137
+ // heir is spent, whatever she then said, and her own key speaks; a heir
138
+ // spent once stays spent.
139
+ //
140
+ // She sent once as the heir and the door did not speak: nothing came
141
+ // back, or bytes that were not Quo's, or the wait ran out. Either the
142
+ // door heard, spent the heir and rotated to the key she announced, or
143
+ // nothing arrived and the heir still stands. Only the far door knows,
144
+ // and it will not say.
110
145
  //
111
146
  // So she asks, and the asking tells her. Under her own key: if the door
112
- // heard, that key is the one it admits, and she is answered. If it did
113
- // not, the key means nothing there, the ask is refused, and a refusal at
114
- // that door writes nothing the heir is untouched and still speaks. Then
115
- // she knocks as the heir, as she would have. One extra round trip in the
116
- // one case where a reply was lost, and the relation is not stranded.
147
+ // heard, that key is the one it admits, and she is answered, or told the
148
+ // door's word. If it did not, the key means nothing there, the ask is
149
+ // refused, and a refusal at that door writes nothing: the heir is
150
+ // untouched and still speaks. Then she knocks as the heir, as she would
151
+ // have. One extra round trip in the one case where a reply was lost, and
152
+ // the relation is not stranded.
117
153
  //
118
154
  // No stranger gains anything by it: whoever holds the invitation could
119
155
  // always knock as the heir, and her own key is admitted only where the
120
156
  // door already bound it to her.
121
157
  if (inv.heir !== undefined && !k.spoke && k.sent) {
122
158
  const own: StandingKeys = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
123
- const r = await inside.send(bind, own, true, method, args, wanted);
159
+ const r = read(await inside.send(bind, own, true, method, args, wanted));
124
160
  k.seq = own.seq;
125
161
  k.current = own.current;
126
162
  k.next = own.next;
127
- if (isUnreached(r)) return r; // still nothing. she knows no more than before.
128
- if (!isSilence(r) && !isWord(r) && 'object' in r) {
129
- k.spoke = true; // it was heard the first time, and this is the answer
130
- bind.answered[name] = true;
131
- return r.object;
163
+ inside.wrote();
164
+ if (!r.door) {
165
+ // Nothing the door said. Unreached and late are hers to hear now; bytes
166
+ // that were not Quo's leave her where she was, and the heir is tried.
167
+ if (!isSilence(r.answer)) return r.answer;
168
+ } else if (!isSilence(r.answer)) {
169
+ // The door spoke to her own key, so it heard the first knock: this is
170
+ // the answer, or the door's word for her. Its silence alone cannot be
171
+ // told from a refusal, so silence is followed by the knock as the heir,
172
+ // which the door then refuses if it had heard, and she hears silence.
173
+ k.spoke = true;
174
+ if (answered(r)) bind.answered[name] = true;
175
+ return r.answer;
132
176
  }
133
- // Refused. The door never heard her, so the heir is still hers to spend.
134
177
  }
135
178
 
136
179
  const first = inv.heir !== undefined && !k.spoke;
137
180
  const keys: StandingKeys = first ? { ward: inv.ward, heir: inv.heir!, current: inv.secret!, next: k.current, seq: k.seq } : { ward: inv.ward, heir: inv.heir ?? null, current: k.current, next: k.next, seq: k.seq };
138
- const r = await inside.send(bind, keys, true, method, args, wanted);
181
+ const r = read(await inside.send(bind, keys, true, method, args, wanted));
139
182
  k.sent = true; // bytes went out. whether they arrived is the far door's to know.
140
183
  // The number she spoke under, and the key she announced, kept whoever
141
184
  // answered and kept when nobody did. A reply lost on the way back is a
@@ -146,16 +189,10 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
146
189
  k.current = keys.current;
147
190
  k.next = keys.next;
148
191
  }
149
- if (isUnreached(r)) return r; // nothing came back. she does not know whether it was heard.
150
- // The heir is spent once the door honoured it, whatever she then said: an
151
- // object, silence, or that she threw. A refusal wrote nothing, and says
152
- // so by its word, so the heir still speaks and the next knock is as it.
153
- k.spoke = !(!isSilence(r) && !isWord(r) && 'quo' in r && r.quo !== 'threw');
154
- if (isSilence(r) || isWord(r)) return r;
155
- if ('silence' in r) return silence;
156
- if ('quo' in r) return word(r.quo);
157
- bind.answered[name] = true;
158
- return r.object;
192
+ if (honoured(r)) k.spoke = true;
193
+ if (answered(r)) bind.answered[name] = true;
194
+ inside.wrote();
195
+ return r.answer;
159
196
  };
160
197
 
161
198
  // The id she took this relation under, if she took it. One relation is one
@@ -172,7 +209,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
172
209
  // standing's keys and its count. A second lane here would speak for the
173
210
  // same relation, and the two would refuse each other.
174
211
  const id = takenAs(nameOf(inv));
175
- 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')))));
212
+ if (id !== undefined) return standingAt(id).ask(method, args, wanted);
176
213
  return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
177
214
  },
178
215
  // Take consumes. Until now the relation lived in the knock record, under
@@ -206,12 +243,14 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
206
243
  put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
207
244
  delete bind.knocks[name];
208
245
  delete bind.answered[name];
246
+ inside.wrote();
209
247
  return Promise.resolve(id);
210
248
  });
211
249
  },
212
250
  remove: (id: string): void => {
213
251
  drop(cells.standings, id);
214
252
  drop(bind.standings, id);
253
+ inside.wrote();
215
254
  },
216
255
  };
217
256
 
@@ -228,13 +267,18 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
228
267
  },
229
268
  });
230
269
 
270
+ // The record an ask writes into is the one it was issued on. A standing
271
+ // dropped and re-taken under the same id while the ask was in flight is
272
+ // another relation, and the blueprint that came back is not hers.
231
273
  const asking = async (id: string, keys: StandingKeys, rec: Cells['standings'][string] | undefined, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
232
274
  const out = await send(keys, method, args, wanted, rec);
233
275
  if (method === undefined && !isSilence(out) && !isWord(out)) {
234
- const here = at(cells.standings, id);
235
- if (here) {
236
- here.blueprint = out as never;
237
- here.digest = here.seen = await digest(out);
276
+ // A blueprint her cells cannot keep, nested past the bound or carrying
277
+ // a key no harbor writes, is not Quo's bytes: silence, and nothing written.
278
+ if (cellFault(out, 'blueprint') !== null) return silence;
279
+ if (rec && at(cells.standings, id) === rec) {
280
+ rec.blueprint = out as never;
281
+ rec.digest = rec.seen = await digest(out);
238
282
  }
239
283
  }
240
284
  return out;
@@ -251,19 +295,43 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
251
295
  cells,
252
296
  // A being may make. The ward refuses what it refuses the owner: a key
253
297
  // already booted, a class the harbor does not hold, and a throw at birth
254
- // leaves the partition as it was. Reaching into the being made is not
255
- // offered here: that is the owner's, and she has no relation to her
256
- // until one is invited and taken like any other.
257
- boot: (className: string, key: string): string | null => inside.instantiate(key, className),
298
+ // leaves the partition as it was. Reaching into another is still the
299
+ // owner's alone: what a maker may have is a relation to what she made,
300
+ // by naming an id, and it is made the way every relation is. The being
301
+ // made mints an occupant for her maker under the maker's own key, so she
302
+ // knows who made her by that name and by nothing else; her maker knocks,
303
+ // takes it under the id she gave, and holds one standing like any other.
304
+ // Nothing half-lives: a relation that could not be made unmakes the
305
+ // being, who is a moment old and known to nobody.
306
+ boot: async (className: string, made: string, id?: string): Promise<string | null> => {
307
+ if (id !== undefined && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))) return null;
308
+ const born = inside.instantiate(made, className);
309
+ if (born === null || id === undefined) return born;
310
+ const inv = await inside.relate(born, key);
311
+ if (inv) {
312
+ const out = await calls.knock(inv);
313
+ if (!isSilence(out) && !isWord(out) && (await calls.take(id, inv)) === id) return born;
314
+ }
315
+ inside.unmake(born);
316
+ return null;
317
+ },
258
318
  occupants: {
259
319
  // rotation one: the ward mints the occupant's first key and gives the secret away.
260
320
  // it keeps the pk beside the id and nothing else. the invitation IS the key.
261
- invite: async (id: string): Promise<Invitation | null> => {
321
+ invite: async (id: string, notes?: JsonObject): Promise<Invitation | null> => {
262
322
  if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
263
- put(cells.occupants, id, { id, notes: {} });
323
+ // The key first, and nothing written until it exists: a record put
324
+ // before the mint would be a record a remove in the meantime drops
325
+ // with no heir to close, and the heir opened after it would name an
326
+ // id nobody holds, for a later invite under that id to resurrect.
264
327
  const k = await inside.mintKey(bind);
328
+ if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null; // taken while the key was minted
329
+ // The notes are the terms the inviter minted under, hers to read on her
330
+ // gate. She may write more later; nobody outside ever writes them.
331
+ put(cells.occupants, id, { id, notes: notes ? { ...notes } : {} });
265
332
  inside.openHeir(k.pk, key, id);
266
333
  put(bind.occupants, id, k.pk);
334
+ inside.wrote();
267
335
  return { ward: inside.pk, heir: k.pk, secret: k.seed };
268
336
  },
269
337
  remove: (id: string): void => {
@@ -271,6 +339,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
271
339
  const heir = at(bind.occupants, id);
272
340
  drop(bind.occupants, id);
273
341
  if (heir) inside.closeHeir(heir); // the heir dies with the id
342
+ inside.wrote();
274
343
  },
275
344
  },
276
345
  standings,
package/src/ward/ward.ts CHANGED
@@ -16,9 +16,11 @@ import { ownerAnswer } from './owner.ts';
16
16
  import { allow, within, LATE } from './allowance.ts';
17
17
  import { beingKey, openReply, sealAsk, wardKey, wardPadlock, wardSignPk, type ReplyPayload, type WardKey } from './seal.ts';
18
18
  import { unhex } from './arithmetic.ts';
19
- import { guardCells, cellFault } from './cells.ts';
19
+ import { guardCells, unguarded, cellFault } from './cells.ts';
20
20
 
21
- type Booted = Door & { bind: Bind; stance: Stance; cells: Cells & { class?: string } };
21
+ // One being of the ward as the ward holds her: her door, her bind table, her
22
+ // stance and her cells. The owner's asks see this and nothing more.
23
+ export type Booted = Door & { bind: Bind; stance: Stance; cells: Cells & { class?: string } };
22
24
 
23
25
  // How many minted pks she keeps behind her. Enough to name the keys in play.
24
26
  const MINTED = 8;
@@ -47,23 +49,24 @@ class Self implements BeingLike {
47
49
  this.g = ground;
48
50
  this.key = key;
49
51
  this.pk = key.pk;
50
- this.p = open(ground.memory); // the partition IS the ward's cells
51
- this.heirs = new Heirs(this.p);
52
+ this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
53
+ this.heirs = new Heirs(this.p, () => this.#wrote());
52
54
  this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
53
55
  this.#boot(this.pk, this); // the ward's ward is itself
54
56
  // a restart is silent: every being in the cells is constructed again, unasked.
55
- // A throw on a boot the owner asked for is that boot's answer: silence,
56
- // and nothing half-lives. This boot nobody asked for, and there is nobody
57
- // to hand a silence to. Any constructor may throw a default that is not
58
- // a value, an author's mistake, a shape she no longer recognises after an
59
- // upgrade — and let through here it is one being taking down the ward and
60
- // every other being in it, on a restart. She is absent instead: no door,
61
- // so silence at the door, and her cells sit untouched, waiting for the
62
- // run that can read them.
57
+ // A throw on a boot the owner asked for is that boot's error, and nothing
58
+ // half-lives. This boot nobody asked for, and there is nobody to hand an
59
+ // error to. Any constructor may throw, a default that is not a value, an
60
+ // author's mistake, a shape she does not recognise after an upgrade, and
61
+ // let through here it is one being taking down the ward and every other
62
+ // being in it, on a restart. She is absent instead: no door, so silence
63
+ // at the door, and her cells sit untouched, waiting for the run that can
64
+ // read them. Her row stays hers: nobody boots another class over it, the
65
+ // owner sees her as absent, and the owner may unboot her.
63
66
  for (const [k, c] of Object.entries(this.p.beings)) {
64
67
  if (k === this.pk || !c.class) continue;
65
68
  try {
66
- this.#instantiate(k, c.class);
69
+ this.#boot(k, c.class);
67
70
  } catch {
68
71
  /* she is not here this run. the ward is, and so is everyone else. */
69
72
  }
@@ -89,10 +92,14 @@ class Self implements BeingLike {
89
92
  {
90
93
  pk: this.pk,
91
94
  doors: this.doors,
95
+ absent: () => this.#absent(),
92
96
  publicKey: () => this.p.public,
93
97
  instantiate: (key, className) => this.#instantiate(key, className),
94
98
  unboot: (key) => this.#unboot(key),
95
- setPublic: (key) => void (this.p.public = key),
99
+ setPublic: (key) => {
100
+ this.p.public = key;
101
+ this.#wrote();
102
+ },
96
103
  },
97
104
  asker,
98
105
  method,
@@ -101,11 +108,37 @@ class Self implements BeingLike {
101
108
  }
102
109
 
103
110
  // ---- ward functions. on the object. no stance reaches them.
104
- #instantiate(key: string, className: string): Booted | null {
105
- if (!key || this.doors.has(key)) return null;
106
- const door = this.#boot(key, className);
111
+
112
+ // The partition was written. The harbor is told, and nothing more: what it
113
+ // does with the word is its own, and the ward never learns.
114
+ #wrote(): void {
115
+ this.g.wrote?.();
116
+ }
117
+
118
+ // The rows no door holds this run: beings whose class threw at birth or
119
+ // is not the harbor's to give. Key to class.
120
+ #absent(): Record<string, string | null> {
121
+ const out: Record<string, string | null> = {};
122
+ for (const [k, c] of Object.entries(this.p.beings)) if (k !== this.pk && !this.doors.has(k)) put(out, k, c.class ?? null);
123
+ return out;
124
+ }
125
+
126
+ // A new being under a key nobody holds. A key with a row in the partition
127
+ // is booted, whether a door holds it this run or not: an absent being's
128
+ // row is hers, with every relation in it, and no class is booted over it.
129
+ // A throw at birth is that boot's refusal and nothing half-lives; the
130
+ // ward says which, since the owner hears objects.
131
+ #instantiate(key: string, className: string): Booted | 'threw' | null {
132
+ if (typeof key !== 'string' || typeof className !== 'string' || !key || this.doors.has(key) || at(this.p.beings, key)) return null;
133
+ let door: Booted | null;
134
+ try {
135
+ door = this.#boot(key, className);
136
+ } catch {
137
+ return 'threw';
138
+ }
107
139
  if (!door) return null;
108
- door.cells.class = className; // so a restart finds her
140
+ unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
141
+ this.#wrote();
109
142
  return door;
110
143
  }
111
144
 
@@ -115,20 +148,30 @@ class Self implements BeingLike {
115
148
  // rather than meeting a being who is simply not there any more. What is
116
149
  // left after that is her cells and her bind table, and they go with her:
117
150
  // a row naming a being no door holds would be read on the next restart
118
- // and boot her again.
151
+ // and boot her again. An absent being leaves the same way: she has no
152
+ // stance to speak for her, so the ward closes her heirs itself.
119
153
  #unboot(key: string): string[] | null {
154
+ if (key === this.pk) return null;
120
155
  const door = this.doors.get(key);
121
- if (!door || key === this.pk) return null;
122
- const occupants = Object.keys(door.cells.occupants),
123
- standings = Object.keys(door.cells.standings);
124
- for (const id of occupants) door.stance.occupants.remove(id);
125
- for (const id of standings) door.stance.standings.remove(id);
156
+ const cells = door?.cells ?? at(this.p.beings, key);
157
+ if (!cells) return null;
158
+ const ids = (r: unknown) => (r !== null && typeof r === 'object' ? Object.keys(r) : []);
159
+ const occupants = ids(cells.occupants),
160
+ standings = ids(cells.standings);
161
+ if (door) {
162
+ for (const id of occupants) door.stance.occupants.remove(id);
163
+ for (const id of standings) door.stance.standings.remove(id);
164
+ } else {
165
+ const bind = at(this.p.bind, key);
166
+ for (const heir of Object.values(bind?.occupants ?? {})) this.heirs.close(heir);
167
+ }
126
168
  // A ward whose public being is gone answers arrivals for no heir with
127
169
  // silence, as a ward that never had one does.
128
170
  if (this.p.public === key) this.p.public = null;
129
171
  drop(this.p.beings, key);
130
172
  drop(this.p.bind, key);
131
173
  this.doors.delete(key);
174
+ this.#wrote();
132
175
  return [...occupants, ...standings];
133
176
  }
134
177
 
@@ -136,7 +179,7 @@ class Self implements BeingLike {
136
179
  // harbor does not know makes no being, and a boot that made nobody must
137
180
  // leave the partition as it found it.
138
181
  #boot(key: string, classNameOrSelf: string | Self): Booted | null {
139
- const cells = guardCells(at(this.p.beings, key) ?? emptyCells());
182
+ const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
140
183
  const bind = at(this.p.bind, key) ?? emptyBind();
141
184
  const stance = buildStance(
142
185
  {
@@ -148,23 +191,27 @@ class Self implements BeingLike {
148
191
  // A throw at birth is null to her, not a throw in her method: she asked
149
192
  // for a being and got none, and her own answer is still hers to give.
150
193
  instantiate: (k, className) => {
151
- try {
152
- return this.#instantiate(k, className)?.key ?? null;
153
- } catch {
154
- return null;
155
- }
194
+ const made = this.#instantiate(k, className);
195
+ return made && made !== 'threw' ? made.key : null;
156
196
  },
197
+ // The two halves of a maker's way back, each the ward's own call: the
198
+ // being made mints an occupant for her maker, and a relation that
199
+ // never came about takes her out again.
200
+ relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
201
+ unmake: (k) => void this.#unboot(k),
202
+ wrote: () => this.#wrote(),
157
203
  },
158
204
  key,
159
205
  cells,
160
206
  bind,
161
207
  );
162
208
  const being = classNameOrSelf === this ? this : this.g.instantiate(classNameOrSelf as string, stance);
163
- if (!being) return null;
209
+ if (!being || typeof being.answer !== 'function') return null; // a being is anything with an answer, and nothing without one
164
210
  put(this.p.beings, key, cells);
165
211
  put(this.p.bind, key, bind);
166
212
  const door: Booted = { key, cells, bind, stance, being };
167
213
  this.doors.set(key, door);
214
+ this.#wrote();
168
215
  return door;
169
216
  }
170
217
 
@@ -176,6 +223,7 @@ class Self implements BeingLike {
176
223
  // but the keys of the moment.
177
224
  bind.minted.push(k.pk);
178
225
  if (bind.minted.length > MINTED) bind.minted.splice(0, bind.minted.length - MINTED);
226
+ this.#wrote();
179
227
  return k;
180
228
  }
181
229
 
@@ -202,10 +250,12 @@ class Self implements BeingLike {
202
250
  // Args that will not go through the seal are args that never left. A
203
251
  // being holds three answers and a throw is not one of them, so an ask
204
252
  // that cannot be sealed is told what is true of it: no door was reached.
205
- // Values only, by the same rule her cells keep: a Date would arrive as a
206
- // string, a function would not arrive at all, and she would never know
207
- // the far being was asked something other than what she said.
208
- if (cellFault(args, 'args') !== null) return unreached();
253
+ // One object of values, by the rule her cells keep and the far door
254
+ // reads: a Date would arrive as a string, a function would not arrive at
255
+ // all, a list or a string would be refused as a stranger's bytes, and
256
+ // she would never know the far being was asked something other than
257
+ // what she said.
258
+ if (args === null || typeof args !== 'object' || Array.isArray(args) || cellFault(args, 'args') !== null) return unreached();
209
259
  let sealed;
210
260
  try {
211
261
  sealed = await sealAsk(keys.heir, { by, next, seq, time: budget.time, method, args }, unhex(keys.current), wardPadlock(keys.ward), this.g.random(32));
@@ -237,6 +287,7 @@ class Self implements BeingLike {
237
287
  keys.current = keys.next; // the far door holds `next` as announced. move to it.
238
288
  keys.next = null;
239
289
  }
290
+ this.#wrote(); // the count moved, and the keys may have
240
291
  return reply;
241
292
  }
242
293
  }
@@ -65,6 +65,13 @@
65
65
  "signature": "60aa62f7129c8fdeba710872fa10016dae1aa12df0f31969a6baabf60b2ebc3d0822cc9a8b1eb12246076b2eea69d381a03c2f581c05ba6bcca3b4ba80c2f504",
66
66
  "refuses": true
67
67
  },
68
+ {
69
+ "name": "a zero signature under a non-canonical small-order public key, y equal to p, is silence",
70
+ "voice": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
71
+ "message": "62792077686f736520617574686f72697479",
72
+ "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
73
+ "refuses": true
74
+ },
68
75
  {
69
76
  "name": "an X25519 agreement",
70
77
  "secret": "fcfabee71b7a33993cca5579e6a273ffd1c62cc1749cbf1c9049f599e44f6477",
@@ -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. 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.",
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 with the heir it is for in front, which is not canonical: these vectors pin the key order sealAsk writes, to, by, next, seq, time, method, args, and to is signed with the rest, null for nobody; hops is in no vector, because nothing sets it. The two signed bodies are the bytes inside the two sealed asks. 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",
@@ -23,11 +23,20 @@
23
23
  "digest": "269c9e90f39f6fcae9deab916e3a4148788dec5ba0d06023edc8e52180c773db"
24
24
  },
25
25
  {
26
- "name": "the signed ask body, and its signature",
27
- "payload": "{\"by\":\"a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0\",\"next\":null,\"seq\":1,\"time\":30000,\"method\":\"hello\",\"args\":{\"name\":\"quo\"}}",
28
- "body": "7b226279223a2261303961613566343761363735393830326666393535663864633264326131346135633939643233626539376638363431323766663933383334353561346630222c226e657874223a6e756c6c2c22736571223a312c2274696d65223a33303030302c226d6574686f64223a2268656c6c6f222c2261726773223a7b226e616d65223a2271756f227d7d",
26
+ "name": "the signed ask body for nobody, and its signature: to is inside, first",
27
+ "to": "",
28
+ "payload": "{\"to\":null,\"by\":\"a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0\",\"next\":null,\"seq\":1,\"time\":30000,\"method\":\"hello\",\"args\":{\"name\":\"quo\"}}",
29
+ "body": "7b22746f223a6e756c6c2c226279223a2261303961613566343761363735393830326666393535663864633264326131346135633939643233626539376638363431323766663933383334353561346630222c226e657874223a6e756c6c2c22736571223a312c2274696d65223a33303030302c226d6574686f64223a2268656c6c6f222c2261726773223a7b226e616d65223a2271756f227d7d",
30
+ "secret": "2222222222222222222222222222222222222222222222222222222222222222",
31
+ "signature": "535c50354632d607fc5b68d081a0191ebf85c296b6c7e67b7b0dcc6030d569673dfe35f164b6b2eb3558c4454c32aabc5586bd82e305401a82fda7a166b91906"
32
+ },
33
+ {
34
+ "name": "the signed ask body to a heir, and its signature: the same payload, to named",
35
+ "to": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
36
+ "payload": "{\"to\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"by\":\"a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0\",\"next\":null,\"seq\":1,\"time\":30000,\"method\":\"hello\",\"args\":{\"name\":\"quo\"}}",
37
+ "body": "7b22746f223a2261616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161222c226279223a2261303961613566343761363735393830326666393535663864633264326131346135633939643233626539376638363431323766663933383334353561346630222c226e657874223a6e756c6c2c22736571223a312c2274696d65223a33303030302c226d6574686f64223a2268656c6c6f222c2261726773223a7b226e616d65223a2271756f227d7d",
29
38
  "secret": "2222222222222222222222222222222222222222222222222222222222222222",
30
- "signature": "21fae42dc81de4a1983ac9dd457338043e3e69ba76331cb5616f3106e007de228fee0f19c49035b90789799667bc7de4d4212618790b2b449e5a26c299134c0b"
39
+ "signature": "1f0e63f365dd09bff2c070091ea209d14eefec1026bf210dba87a4a18135fd90107c9879177695f0b4a899e67777b75d6001229e0875657cfc9a2cd197880500"
31
40
  },
32
41
  {
33
42
  "name": "a sealed ask to a heir: the ephemeral pk, then the box, and nothing outside it",