@quo-systems/quo 0.2.10 → 0.2.12

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 (72) hide show
  1. package/SPEC.md +141 -22
  2. package/dist/being/being.js +7 -1
  3. package/dist/being/digest.js +15 -1
  4. package/dist/being/index.d.ts +2 -2
  5. package/dist/being/index.js +2 -2
  6. package/dist/being/silence.d.ts +2 -0
  7. package/dist/being/silence.js +12 -0
  8. package/dist/being/types.d.ts +2 -0
  9. package/dist/being/types.js +25 -0
  10. package/dist/conformance/assert.js +40 -6
  11. package/dist/conformance/beings.d.ts +48 -5
  12. package/dist/conformance/beings.js +39 -8
  13. package/dist/conformance/estate.js +109 -18
  14. package/dist/conformance/index.d.ts +5 -2
  15. package/dist/conformance/index.js +164 -6
  16. package/dist/harbor/core.d.ts +3 -2
  17. package/dist/harbor/core.js +86 -25
  18. package/dist/harbor/dial.js +28 -13
  19. package/dist/harbor/index.d.ts +1 -0
  20. package/dist/harbor/index.js +3 -0
  21. package/dist/harbor/memory.d.ts +3 -3
  22. package/dist/harbor/memory.js +6 -12
  23. package/dist/harbor/reach.js +10 -10
  24. package/dist/ward/allowance.js +7 -2
  25. package/dist/ward/arithmetic.d.ts +1 -0
  26. package/dist/ward/arithmetic.js +11 -3
  27. package/dist/ward/door.d.ts +2 -2
  28. package/dist/ward/door.js +14 -5
  29. package/dist/ward/ground.d.ts +4 -1
  30. package/dist/ward/ground.js +38 -1
  31. package/dist/ward/heirs.d.ts +1 -2
  32. package/dist/ward/heirs.js +13 -14
  33. package/dist/ward/index.d.ts +1 -0
  34. package/dist/ward/index.js +3 -0
  35. package/dist/ward/owner.d.ts +4 -4
  36. package/dist/ward/owner.js +36 -19
  37. package/dist/ward/partition.d.ts +3 -0
  38. package/dist/ward/partition.js +109 -4
  39. package/dist/ward/seal.d.ts +1 -0
  40. package/dist/ward/seal.js +38 -13
  41. package/dist/ward/stance.d.ts +3 -2
  42. package/dist/ward/stance.js +48 -20
  43. package/dist/ward/ward.d.ts +1 -1
  44. package/dist/ward/ward.js +40 -19
  45. package/package.json +1 -1
  46. package/src/being/being.ts +7 -1
  47. package/src/being/digest.ts +15 -2
  48. package/src/being/index.ts +2 -2
  49. package/src/being/silence.ts +14 -0
  50. package/src/being/types.ts +29 -1
  51. package/src/conformance/assert.ts +37 -4
  52. package/src/conformance/beings.ts +41 -10
  53. package/src/conformance/estate.ts +106 -17
  54. package/src/conformance/index.ts +187 -12
  55. package/src/harbor/core.ts +98 -25
  56. package/src/harbor/dial.ts +41 -14
  57. package/src/harbor/index.ts +3 -0
  58. package/src/harbor/memory.ts +8 -14
  59. package/src/harbor/reach.ts +10 -11
  60. package/src/ward/allowance.ts +7 -2
  61. package/src/ward/arithmetic.ts +14 -5
  62. package/src/ward/door.ts +16 -7
  63. package/src/ward/ground.ts +43 -1
  64. package/src/ward/heirs.ts +13 -14
  65. package/src/ward/index.ts +3 -0
  66. package/src/ward/owner.ts +45 -26
  67. package/src/ward/partition.ts +109 -5
  68. package/src/ward/seal.ts +39 -12
  69. package/src/ward/stance.ts +53 -23
  70. package/src/ward/ward.ts +46 -25
  71. package/vectors/framing.json +16 -10
  72. package/vectors/wire.json +4 -4
@@ -3,24 +3,30 @@
3
3
  // outer is in what she holds: ids in, values out, keys in the bind table.
4
4
  import { silence, isSilence, isWord, word, wordOf } from '../being/silence.ts';
5
5
  import { digest } from '../being/digest.ts';
6
- import { RESERVED_IDS } from '../being/types.ts';
6
+ import { RESERVED_IDS, isBlueprint } from '../being/types.ts';
7
7
  import type { Answer, Cells, Invitation, JsonObject, Silence, Stance, Standing, Standings, Wanted, Word } from '../being/types.ts';
8
- import { at, put, drop, type Bind, type StandingKeys } from './partition.ts';
8
+ import { at, put, drop, prune, type Bind, type StandingKeys } from './partition.ts';
9
9
  import { allow, within, LATE } from './allowance.ts';
10
- import { isWardPk, type ReplyPayload } from './seal.ts';
10
+ import { isHex, isWardPk, type ReplyPayload } from './seal.ts';
11
11
  import { cellFault } from './cells.ts';
12
12
 
13
13
  // What the stance needs from its ward, and no more.
14
14
  export type Inside = {
15
15
  pk: string;
16
+ // Is the being this stance was built for still one of the ward's? Unboot
17
+ // takes her door and her rows, but a class whose code is still running
18
+ // holds the stance closure, and everything in it still points at live
19
+ // objects. Read before every call, so a being taken out reaches nothing.
20
+ live(): boolean;
16
21
  mintKey(bind: Bind): Promise<{ seed: string; pk: string }>; // a fresh signing key, recorded as minted
17
22
  openHeir(heir: string, being: string, id: string): void;
18
23
  closeHeir(heir: string): void;
19
24
  // one send for every destination. the far door's reply, silence, or unreached.
20
- // signs as keys.current, announces keys.next (minting one if asked), and rotates keys on an answer.
21
- send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
25
+ // signs as keys.current, mints and announces keys.next where the relation has
26
+ // a heir to rotate on, and rotates keys on an answer.
27
+ send(bind: Bind, keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
22
28
  // a new being of this ward, as the owner's boot makes one. the key, or null.
23
- instantiate(key: string, className: string): string | null;
29
+ instantiate(className: string, key: string): string | null;
24
30
  // mint an occupant on a being of this ward, for the being who made her.
25
31
  relate(key: string, id: string): Promise<Invitation | null>;
26
32
  // take a being of this ward out again, with every relation she holds.
@@ -52,7 +58,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
52
58
  const i = inv as SealedInvitation;
53
59
  if (!i || typeof i !== 'object' || !isWardPk(i.ward)) return false;
54
60
  if (i.heir === undefined) return i.secret === undefined;
55
- return typeof i.heir === 'string' && typeof i.secret === 'string' && /^[0-9a-f]{64}$/.test(i.heir) && /^[0-9a-f]{64}$/.test(i.secret);
61
+ return isHex(i.heir) && isHex(i.secret);
56
62
  };
57
63
 
58
64
  // One relation, one ask at a time. The rotation is a conversation: a send
@@ -114,13 +120,19 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
114
120
  const honoured = (r: Read): boolean => r.door && !(isWord(r.answer) && wordOf(r.answer) !== 'threw');
115
121
  const answered = (r: Read): boolean => r.door && !isSilence(r.answer) && !isWord(r.answer);
116
122
 
117
- const send = async (keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, rec?: Cells['standings'][string]): Promise<Answer> => {
118
- const r = read(await inside.send(bind, keys, true, method, args, wanted));
119
- if (rec && method !== undefined && r.seen !== null) rec.seen = r.seen; // the digest rode along
123
+ // The ask is bounded twice with one budget, once around the lane and once
124
+ // around the wire, and the two bells are microseconds apart. A reply that
125
+ // lands between them is a reply she was already told was late: its keys are
126
+ // rotated, since the far door rotated too and nothing may un-rotate that,
127
+ // but nothing of it is written into her cells. What she was handed is the
128
+ // last word on what she saw.
129
+ const send = async (keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, rec?: Cells['standings'][string], live: () => boolean = () => true): Promise<Answer> => {
130
+ const r = read(await inside.send(bind, keys, method, args, wanted));
131
+ if (rec && method !== undefined && r.seen !== null && live()) rec.seen = r.seen; // the digest rode along
120
132
  return r.answer;
121
133
  };
122
134
 
123
- const knocking = async (inv: SealedInvitation, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
135
+ const knocking = async (inv: SealedInvitation, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, live: () => boolean = () => true): Promise<Answer> => {
124
136
  const name = nameOf(inv);
125
137
  // Taken while this knock waited its turn: the relation lives in the
126
138
  // standing now and speaks on its lane, under its keys and its count.
@@ -128,10 +140,11 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
128
140
  if (taken !== undefined) {
129
141
  const keys = at(bind.standings, taken);
130
142
  if (!keys) return word('dropped');
131
- return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted));
143
+ return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted, live));
132
144
  }
133
145
  // her own key for this relation, minted once: rotation two. reused if she knocks again.
134
146
  const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
147
+ prune(bind.knocks, bind.answered); // this one is the newest, so it stays and the oldest never answered goes
135
148
  // The first knock on a heir speaks as the heir, the key the inviter
136
149
  // handed out, and announces her own. Once the door has honoured it the
137
150
  // heir is spent, whatever she then said, and her own key speaks; a heir
@@ -156,7 +169,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
156
169
  // door already bound it to her.
157
170
  if (inv.heir !== undefined && !k.spoke && k.sent) {
158
171
  const own: StandingKeys = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
159
- const r = read(await inside.send(bind, own, true, method, args, wanted));
172
+ const r = read(await inside.send(bind, own, method, args, wanted));
160
173
  k.seq = own.seq;
161
174
  k.current = own.current;
162
175
  k.next = own.next;
@@ -178,7 +191,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
178
191
 
179
192
  const first = inv.heir !== undefined && !k.spoke;
180
193
  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 };
181
- const r = read(await inside.send(bind, keys, true, method, args, wanted));
194
+ const r = read(await inside.send(bind, keys, method, args, wanted));
182
195
  k.sent = true; // bytes went out. whether they arrived is the far door's to know.
183
196
  // The number she spoke under, and the key she announced, kept whoever
184
197
  // answered and kept when nobody did. A reply lost on the way back is a
@@ -201,8 +214,16 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
201
214
  const keyName = (k: StandingKeys) => (k.heir === null ? `public:${k.ward}` : `${k.ward}:${k.heir}`);
202
215
  const takenAs = (name: string): string | undefined => Object.keys(bind.standings).find((id) => keyName(bind.standings[id]) === name);
203
216
 
217
+ // A being the ward has taken out reaches nothing through the stance she
218
+ // still holds. Her relations went with her, so every one of them reads as
219
+ // dropped, and nothing she asks for is made: a heir written here would name
220
+ // a being no door holds, and an id minted here would be handed to whoever
221
+ // is booted under her key next.
222
+ const gone = () => !inside.live();
223
+
204
224
  const calls = {
205
225
  knock: async (inv: Invitation, method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
226
+ if (gone()) return word('dropped');
206
227
  if (!valid(inv)) return word('invitation'); // S1. nothing is sent
207
228
  // She may knock again, and after take that knock is an ask: the relation
208
229
  // has one home, so it answers on the standing's lane, under the
@@ -210,7 +231,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
210
231
  // same relation, and the two would refuse each other.
211
232
  const id = takenAs(nameOf(inv));
212
233
  if (id !== undefined) return standingAt(id).ask(method, args, wanted);
213
- return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
234
+ return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted, live) : Promise.resolve(word('late')))));
214
235
  },
215
236
  // Take consumes. Until now the relation lived in the knock record, under
216
237
  // the ward and the heir; from now it lives in the standing, under her id.
@@ -230,7 +251,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
230
251
  // ask on it is refused. The relation she took correctly would be dead
231
252
  // because a second knock on the same invitation was careless.
232
253
  take: async (id: string, inv: Invitation): Promise<string | null> => {
233
- if (!valid(inv)) return null;
254
+ if (gone() || !valid(inv)) return null;
234
255
  const name = nameOf(inv);
235
256
  return lane(`knock:${name}`, () => {
236
257
  if (!bind.answered[name]) return Promise.resolve(null);
@@ -248,6 +269,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
248
269
  });
249
270
  },
250
271
  remove: (id: string): void => {
272
+ if (gone()) return;
251
273
  drop(cells.standings, id);
252
274
  drop(bind.standings, id);
253
275
  inside.wrote();
@@ -260,24 +282,30 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
260
282
  // issued while the standing stood is answered even if she drops it in
261
283
  // the next line. Waiting for the lane is not a reason to lose it.
262
284
  ask: (method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
285
+ if (gone()) return Promise.resolve(word('dropped') as Answer);
263
286
  const keys = at(bind.standings, id);
264
287
  if (!keys) return Promise.resolve(word('dropped') as Answer); // S2. she dropped it between one line and the next
265
288
  const rec = at(cells.standings, id);
266
- return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(word('late')))));
289
+ return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted, live) : Promise.resolve(word('late')))));
267
290
  },
268
291
  });
269
292
 
270
293
  // The record an ask writes into is the one it was issued on. A standing
271
294
  // dropped and re-taken under the same id while the ask was in flight is
272
295
  // another relation, and the blueprint that came back is not hers.
273
- const asking = async (id: string, keys: StandingKeys, rec: Cells['standings'][string] | undefined, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
274
- const out = await send(keys, method, args, wanted, rec);
296
+ const asking = async (id: string, keys: StandingKeys, rec: Cells['standings'][string] | undefined, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, live: () => boolean = () => true): Promise<Answer> => {
297
+ const out = await send(keys, method, args, wanted, rec, live);
275
298
  if (method === undefined && !isSilence(out) && !isWord(out)) {
276
299
  // A blueprint her cells cannot keep, nested past the bound or carrying
277
300
  // a key no harbor writes, is not Quo's bytes: silence, and nothing written.
278
301
  if (cellFault(out, 'blueprint') !== null) return silence;
279
- if (rec && at(cells.standings, id) === rec) {
280
- rec.blueprint = out as never;
302
+ if (rec && live() && at(cells.standings, id) === rec) {
303
+ // Read as a blueprint before it is kept as one. A far describe is
304
+ // somebody else's code and may answer anything; what is not a
305
+ // blueprint is not written as one, and she holds no blueprint for
306
+ // that relation rather than one a side would walk and break on. The
307
+ // answer itself is hers either way: it is returned below, unread.
308
+ rec.blueprint = isBlueprint(out) ? out : null;
281
309
  rec.digest = rec.seen = await digest(out);
282
310
  }
283
311
  }
@@ -304,8 +332,9 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
304
332
  // Nothing half-lives: a relation that could not be made unmakes the
305
333
  // being, who is a moment old and known to nobody.
306
334
  boot: async (className: string, made: string, id?: string): Promise<string | null> => {
335
+ if (gone()) return null;
307
336
  if (id !== undefined && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))) return null;
308
- const born = inside.instantiate(made, className);
337
+ const born = inside.instantiate(className, made);
309
338
  if (born === null || id === undefined) return born;
310
339
  const inv = await inside.relate(born, key);
311
340
  if (inv) {
@@ -319,7 +348,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
319
348
  // rotation one: the ward mints the occupant's first key and gives the secret away.
320
349
  // it keeps the pk beside the id and nothing else. the invitation IS the key.
321
350
  invite: async (id: string, notes?: JsonObject): Promise<Invitation | null> => {
322
- if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
351
+ if (gone() || reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
323
352
  // The key first, and nothing written until it exists: a record put
324
353
  // before the mint would be a record a remove in the meantime drops
325
354
  // with no heir to close, and the heir opened after it would name an
@@ -335,6 +364,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
335
364
  return { ward: inside.pk, heir: k.pk, secret: k.seed };
336
365
  },
337
366
  remove: (id: string): void => {
367
+ if (gone()) return;
338
368
  drop(cells.occupants, id);
339
369
  const heir = at(bind.occupants, id);
340
370
  drop(bind.occupants, id);
package/src/ward/ward.ts CHANGED
@@ -8,7 +8,7 @@ import { silence, isSilence, unreached, isWord, word } from '../being/silence.ts
8
8
  import { OWNER } from '../being/types.ts';
9
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
- import { at, drop, open, put, emptyBind, emptyCells, type Bind, type Partition, type StandingKeys } from './partition.ts';
11
+ import { at, drop, open, put, emptyBind, emptyCells, MINTED, type Bind, type Partition, type StandingKeys } from './partition.ts';
12
12
  import { Heirs } from './heirs.ts';
13
13
  import { makeDoor, type Door } from './door.ts';
14
14
  import { buildStance } from './stance.ts';
@@ -19,11 +19,10 @@ import { unhex } from './arithmetic.ts';
19
19
  import { guardCells, unguarded, cellFault } from './cells.ts';
20
20
 
21
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 } };
24
-
25
- // How many minted pks she keeps behind her. Enough to name the keys in play.
26
- const MINTED = 8;
22
+ // stance and her cells. The owner's asks see this and nothing more. Not
23
+ // `Booted`, which is a harbor's word for a ward it started: this is a ward's
24
+ // word for a being living in it, and the two are one module apart.
25
+ export type Resident = Door & { bind: Bind; stance: Stance; cells: Cells & { class?: string } };
27
26
 
28
27
  // The ward. One call in, two pointers out. Async because keys are derived.
29
28
  export async function Ward(ground: Ground): Promise<WardPointers> {
@@ -37,7 +36,8 @@ class Self implements BeingLike {
37
36
  readonly g: Ground;
38
37
  readonly p: Partition;
39
38
  readonly heirs: Heirs;
40
- readonly doors = new Map<string, Booted>();
39
+ readonly doors = new Map<string, Resident>();
40
+ readonly #birthing = new Set<string>(); // keys whose constructor is running: taken, though no door and no row say so yet
41
41
  readonly door: WardPointers['door'];
42
42
  readonly ask: WardPointers['ask'];
43
43
 
@@ -52,7 +52,7 @@ class Self implements BeingLike {
52
52
  this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
53
53
  this.heirs = new Heirs(this.p, () => this.#wrote());
54
54
  this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
55
- this.#boot(this.pk, this); // the ward's ward is itself
55
+ this.#boot(this.pk, () => this); // the ward's ward is itself
56
56
  // a restart is silent: every being in the cells is constructed again, unasked.
57
57
  // A throw on a boot the owner asked for is that boot's error, and nothing
58
58
  // half-lives. This boot nobody asked for, and there is nobody to hand an
@@ -64,9 +64,10 @@ class Self implements BeingLike {
64
64
  // read them. Her row stays hers: nobody boots another class over it, the
65
65
  // owner sees her as absent, and the owner may unboot her.
66
66
  for (const [k, c] of Object.entries(this.p.beings)) {
67
- if (k === this.pk || !c.class) continue;
67
+ const className = c.class;
68
+ if (k === this.pk || !className) continue;
68
69
  try {
69
- this.#boot(k, c.class);
70
+ this.#boot(k, (stance) => this.g.instantiate(className, stance));
70
71
  } catch {
71
72
  /* she is not here this run. the ward is, and so is everyone else. */
72
73
  }
@@ -87,14 +88,18 @@ class Self implements BeingLike {
87
88
  // can invite. A stranger cannot be a named asker here, because the door
88
89
  // names nobody the ward did not bind.
89
90
  async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
90
- if (asker.id === undefined) return { error: 'unknown ask' };
91
+ // Nobody is not an owner. The door names nobody the ward did not bind, so
92
+ // the only way here unnamed is a partition that says the ward is its own
93
+ // public being, which no owner ask will write. Silence, not an error
94
+ // object: an unnamed asker is a stranger, and a stranger hears one thing.
95
+ if (asker.id === undefined) return silence;
91
96
  return ownerAnswer(
92
97
  {
93
98
  pk: this.pk,
94
99
  doors: this.doors,
95
100
  absent: () => this.#absent(),
96
101
  publicKey: () => this.p.public,
97
- instantiate: (key, className) => this.#instantiate(key, className),
102
+ instantiate: (className, key) => this.#instantiate(className, key),
98
103
  unboot: (key) => this.#unboot(key),
99
104
  setPublic: (key) => {
100
105
  this.p.public = key;
@@ -128,13 +133,21 @@ class Self implements BeingLike {
128
133
  // row is hers, with every relation in it, and no class is booted over it.
129
134
  // A throw at birth is that boot's refusal and nothing half-lives; the
130
135
  // 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;
136
+ #instantiate(className: string, key: string): Resident | 'threw' | null {
137
+ // The three refusals are read before the constructor runs, and the
138
+ // constructor can reach the stance and boot: a class that boots its own
139
+ // key from inside itself would pass all three, since neither its door nor
140
+ // its row exists yet, and the two objects would then share one row. A key
141
+ // being born is a key taken, for as long as the birth lasts.
142
+ if (typeof key !== 'string' || typeof className !== 'string' || !key || this.doors.has(key) || at(this.p.beings, key) || this.#birthing.has(key)) return null;
143
+ let door: Resident | null;
144
+ this.#birthing.add(key);
134
145
  try {
135
- door = this.#boot(key, className);
146
+ door = this.#boot(key, (stance) => this.g.instantiate(className, stance));
136
147
  } catch {
137
148
  return 'threw';
149
+ } finally {
150
+ this.#birthing.delete(key);
138
151
  }
139
152
  if (!door) return null;
140
153
  unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
@@ -178,20 +191,27 @@ class Self implements BeingLike {
178
191
  // Nothing is written until there is somebody to write it for: a class the
179
192
  // harbor does not know makes no being, and a boot that made nobody must
180
193
  // leave the partition as it found it.
181
- #boot(key: string, classNameOrSelf: string | Self): Booted | null {
194
+ //
195
+ // `make` is who to build with the stance this raises. Two callers and two
196
+ // makers: the ground's `instantiate` for a being of the ward, and the ward
197
+ // itself, which is the first being in its own map and is already made.
198
+ #boot(key: string, make: (stance: Stance) => BeingLike | null): Resident | null {
182
199
  const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
183
200
  const bind = at(this.p.bind, key) ?? emptyBind();
184
201
  const stance = buildStance(
185
202
  {
186
203
  pk: this.pk,
204
+ // This stance, not merely this key: unboot and boot again under the
205
+ // same key makes a new being, and the old stance is not hers.
206
+ live: () => this.doors.get(key)?.stance === stance,
187
207
  mintKey: (b) => this.#mintKey(b),
188
208
  openHeir: (heir, being, id) => this.heirs.open(heir, being, id),
189
209
  closeHeir: (heir) => this.heirs.close(heir),
190
- send: (b, keys, announce, method, args, wanted) => this.#send(b, keys, announce, method, args, wanted),
210
+ send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted),
191
211
  // A throw at birth is null to her, not a throw in her method: she asked
192
212
  // for a being and got none, and her own answer is still hers to give.
193
- instantiate: (k, className) => {
194
- const made = this.#instantiate(k, className);
213
+ instantiate: (className, k) => {
214
+ const made = this.#instantiate(className, k);
195
215
  return made && made !== 'threw' ? made.key : null;
196
216
  },
197
217
  // The two halves of a maker's way back, each the ward's own call: the
@@ -205,11 +225,11 @@ class Self implements BeingLike {
205
225
  cells,
206
226
  bind,
207
227
  );
208
- const being = classNameOrSelf === this ? this : this.g.instantiate(classNameOrSelf as string, stance);
228
+ const being = make(stance);
209
229
  if (!being || typeof being.answer !== 'function') return null; // a being is anything with an answer, and nothing without one
210
230
  put(this.p.beings, key, cells);
211
231
  put(this.p.bind, key, bind);
212
- const door: Booted = { key, cells, bind, stance, being };
232
+ const door: Resident = { key, cells, bind, stance, being };
213
233
  this.doors.set(key, door);
214
234
  this.#wrote();
215
235
  return door;
@@ -232,7 +252,7 @@ class Self implements BeingLike {
232
252
  // next, seals to the far ward, opens the reply with the ephemeral secret,
233
253
  // and rotates to the announced key once the far door has answered under
234
254
  // the current one.
235
- async #send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted?: Wanted): Promise<ReplyPayload | typeof silence | Word> {
255
+ async #send(bind: Bind, keys: StandingKeys, method: string | undefined, args: JsonObject, wanted?: Wanted): Promise<ReplyPayload | typeof silence | Word> {
236
256
  // What she asked for, held to what this ward allows. Asking for nothing is
237
257
  // the default, and asking for more than the ceiling is the ceiling: budget
238
258
  // is granted by a ward, never minted by a being.
@@ -240,8 +260,9 @@ class Self implements BeingLike {
240
260
  const by = (await beingKey(unhex(keys.current))).pk;
241
261
  // A public being is reached without a heir, and the door keeps nothing for
242
262
  // whoever asked: no key it vouched for, so nothing to announce and nothing
243
- // to rotate to. A standing on her signs with one key for life.
244
- if (announce && keys.next === null && keys.heir !== null) keys.next = (await this.#mintKey(bind)).seed;
263
+ // to rotate to. A standing on her signs with one key for life. Every other
264
+ // ask announces its next: there is no send that does not.
265
+ if (keys.next === null && keys.heir !== null) keys.next = (await this.#mintKey(bind)).seed;
245
266
  const next = keys.next === null ? null : (await beingKey(unhex(keys.next))).pk;
246
267
  // Her count for this relation, one higher every call and never reused. The
247
268
  // far door honours each number once. One relation sends one at a time, so
@@ -7,9 +7,9 @@
7
7
  {
8
8
  "name": "a ward pk from a fixed seed",
9
9
  "seed": "1111111111111111111111111111111111111111111111111111111111111111",
10
- "pk": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c97787377b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13",
11
- "signPk": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737",
12
- "padlockPk": "7b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13"
10
+ "pk": "6d773f8156c80eb9f34d0734e889d824a0d0515f527b853771208ef1ed3feb281fc3da3532de3b20deafd145e30c3de979917e96c22f6c3113a731379eeb3200",
11
+ "signPk": "6d773f8156c80eb9f34d0734e889d824a0d0515f527b853771208ef1ed3feb28",
12
+ "padlockPk": "1fc3da3532de3b20deafd145e30c3de979917e96c22f6c3113a731379eeb3200"
13
13
  },
14
14
  {
15
15
  "name": "a being key from a fixed secret",
@@ -22,6 +22,12 @@
22
22
  "canonical": "{\"asks\":{\"hello\":{\"note\":\"z\"}},\"list\":[1,2],\"note\":null}",
23
23
  "digest": "269c9e90f39f6fcae9deab916e3a4148788dec5ba0d06023edc8e52180c773db"
24
24
  },
25
+ {
26
+ "name": "the digest where JCS is hard: UTF-16 key order and ES numbers",
27
+ "blueprint": "{\"asks\":[],\"notes\":{\"1\":\"One\",\"\u20ac\":\"Euro Sign\",\"\\r\":\"Carriage Return\",\"\ufb33\":\"Hebrew Letter Dalet With Dagesh\",\"\ud83d\ude00\":\"Emoji: Grinning Face\",\"\u0080\":\"Control\",\"\u00f6\":\"Latin Small Letter O With Diaeresis\"},\"numbers\":[1e+21,0,0.1,1e-7,9007199254740992,0.3333333333333333]}",
28
+ "canonical": "{\"asks\":[],\"notes\":{\"\\r\":\"Carriage Return\",\"1\":\"One\",\"\u0080\":\"Control\",\"\u00f6\":\"Latin Small Letter O With Diaeresis\",\"\u20ac\":\"Euro Sign\",\"\ud83d\ude00\":\"Emoji: Grinning Face\",\"\ufb33\":\"Hebrew Letter Dalet With Dagesh\"},\"numbers\":[1e+21,0,0.1,1e-7,9007199254740992,0.3333333333333333]}",
29
+ "digest": "dc5306089d6bf46d9f91d25290b770c0f18955f1192525b51389fd4c23e02089"
30
+ },
25
31
  {
26
32
  "name": "the signed ask body for nobody, and its signature: to is inside, first",
27
33
  "to": "",
@@ -42,37 +48,37 @@
42
48
  "name": "a sealed ask to a heir: the ephemeral pk, then the box, and nothing outside it",
43
49
  "to": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
44
50
  "ephemeralSeed": "3333333333333333333333333333333333333333333333333333333333333333",
45
- "bytes": "7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b14ed97840c789fc01f8b362dff17c11793d230bc13c4f56401abbbd7c4d26ac0379164fc07794e29b8c9330328f8ae2b3b6280d1fb1ced6ef44c54478381be4834444d3c7c4c7258d7653f481e9bc5cc78394fc9d6742edef6a13e3273f24ab212205f83ca25640d301110062940e3bf2cde21dd1707111e240a6de0674a98ef941decb3e0112bcb444576485a180437b1d2ddb5e0773a527c12d781bc669572bafe8d0f1c31396b5e25e354309d9cd8584c8eaa7c6b0ffc97ca3963a997a289254e498dc597b196720a0a70860815f474c56aee9e0d6a1bd2949d314819e9ea7565e379f8d4d8f60bc4243b117ba9b39ab74ef949f7c916843da444c5387118697317ab720ffd10e9954db6e766664e4e7f9e9e09f05df2dc1ff7f47a4fbd419964db60f395043f6a3a"
51
+ "bytes": "7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b140f393feeee5faf9a2707cd3e47c06e7edfd9f5b705ea3ee1a2a2a25b591b37a0af5a068c6455d3cc2dc2948615faf29b182a81ea3d73c166e68f4bb1578c3fbaf22f1dea223d9b84e7e5b90f65139bba894cee2bf68ced2192e548c174bc593dd16d124eb21d3bbde81169bab147676bb11869e08dedb2b15a782d6236ff2ae6b9fa578be8008707ca238f28d39f5029a0340b2e5299e97e44ce9140f8a40ad038d28c931d7872bcfa06f1e29ec964491d4b4ee425ab55b3a09a501a50b6c36f159962247650ec31c42b0c711d97dfc832086366e7c856d332e5bccce04c44c7b67be20bb35d52cccf3a4bf1055fa2a38246c61adc2114da603bd419885b79eb9e3f429d4e26e808beb562d5c60e635649acda8170834f68aee612b56416d369cc72edc8ac1b6204e7"
46
52
  },
47
53
  {
48
54
  "name": "a sealed ask for nobody: the same shape, with to null inside",
49
55
  "to": "",
50
56
  "ephemeralSeed": "3333333333333333333333333333333333333333333333333333333333333333",
51
- "bytes": "7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b14ed97840c789f8c0b863b60bc14d954c89130ed4bc4f53006feedd793843e986ec037fb00211a7dbf9036017bfdfd2b6b378085f944b56ba71e5743dbd7b91163111d6f2a4b7500c6712e1e528c9eda7f395484952f2d92b5e2326b33a71eae02355ccbdd2a331976064b0f7e03e8b87ddb72d402414e4832073bb323488cb3c043b7baa4043a920516305f01574069f2939af9bf2d3f05295edf908316e73499bd7932d14673cfb6e89f68b52b6e7edebb44f36a03b7f4c87b723eb601f2142d97d94ad2ae07b44d1a9ab7f1e0c91bf2e210d66d85a298c9509b39a652868391296be1e21fcc909f97305b"
57
+ "bytes": "7b0d47d93427f8311160781c7c733fd89f88970aef490d8aa0ee19a4cb8a1b140f393feeee5fe38e2a0a807d44d82d259cd9a4ef05ea6ae6f7f4a20c0f4f6ff9fe09018b3c0187cb74c796d510a9f2cb4d2ad5e8652bc435b48c4fe9018b66eda77f4ebc253ac395f3f4ef4372488dbd8957a368ad8fa162d1e9118121e8452dc46e5a59bd4a2ffbff4a60edf24c603ab44b60f5cbb2e4a7572e7e2634eb76b2e7a15ecffd11de46996598739cdb0e6ae1734771089cbe2b08c6807f88d64cf37b26b15e6a32d654377acd67283bc2cfea8117f24d135dec11d10d05c6e65e67cc09a5334fe6ce0ed4bbcb06f54b304e15725b956f00d5c8f6e3b48e4b634b743c581e15f86f7b17b52a96"
52
58
  },
53
59
  {
54
60
  "name": "the sealed silence reply",
55
61
  "reply": "{\"silence\":true}",
56
62
  "replySeed": "4444444444444444444444444444444444444444444444444444444444444444",
57
- "bytes": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b76ec8fe27c6573176a8974f1235379842eeabe08f92c8aae00e383195ff41cbb2124de81119eb83326fbb9b8ab00a149ce41ea21f6c056a324333cc76b82c93ac93fbcc3fc74f57a1bff4a07a0c7824ed5516b7fb8a14006a25f6c28d95e4e64"
63
+ "bytes": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b76ec8fe27c6573176a8974f123537984bdd8baea7cbd4e9b81f38db22c5917e9674bc082183e532adba06227f7e023955f16afcb723ca826a579fc08af3306824c8f8012687532ad79d81b77a7c84a429d151cc593b81312bc44433571a0b4aa"
58
64
  },
59
65
  {
60
66
  "name": "a sealed object reply",
61
67
  "reply": "{\"object\":{\"hi\":\"quo\"},\"seen\":null}",
62
68
  "replySeed": "4444444444444444444444444444444444444444444444444444444444444444",
63
- "bytes": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b76ec93e97a657e002d9135a7394f3ec33bcd723c9d8947909f4cc659d8dfeed743f5b64bfa87b4ac79074219c1032037de444400b509f31559331b7995f9dfbadbe7fb0f6d46db2257db70f248b7739b3baa2ca79b07b66b29ea680faf79340f2309163c9e3c65fcdb33297c419d1cd8517647"
69
+ "bytes": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b76ec93e97a657e002d9135a7394f3ec33bcd723c9d8947909f4cc659d8dfeed743f5b6012ed80af39f9d1bc16c995f69944a0c94244e349308704f2070a98b59a5a4f3e9b1e2a1bfe2e7bcba2446de1fbf84dc15cc5fa5ac03a175cce7d2eaa07b0e1148416580932306d41dc961f145fa7141"
64
70
  },
65
71
  {
66
72
  "name": "a sealed word reply: the ward's word to a key it has bound",
67
73
  "reply": "{\"quo\":\"removed\"}",
68
74
  "replySeed": "4444444444444444444444444444444444444444444444444444444444444444",
69
- "bytes": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b76ec8dfe7f2227567dce23ea274378db64eb74efed5c91135d62d89db62db7d8ffbd54cf3e82e3272d0b26d59130020755b442a16340798d3eec60bc3a3875020da9276d5e72d712ac3dad7ef287d3c153a0588bf4951b9d3540b1142bd54875a3"
75
+ "bytes": "ff2ee45601ec1b67310c7790404585ae697331eee1c1f8cf2419731c1fff3e6b76ec8dfe7f2227567dce23ea274378db6487fe336070ff4780aec6e6cb89197c05df879e39252f523fc910bf2268ded0d2f323f395a4e2352a5004424b828c43f3163595c67c120e5ea029cfecec4abe575fb4327449cd6003634f30b3cc056a54"
70
76
  },
71
77
  {
72
78
  "name": "an invitation for a heir: the ward pk, the heir pk, the heir secret, and nothing else",
73
79
  "wardSeed": "1111111111111111111111111111111111111111111111111111111111111111",
74
80
  "heirSecret": "4444444444444444444444444444444444444444444444444444444444444444",
75
- "invitation": "{\"ward\":\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c97787377b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13\",\"heir\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"secret\":\"4444444444444444444444444444444444444444444444444444444444444444\"}"
81
+ "invitation": "{\"ward\":\"6d773f8156c80eb9f34d0734e889d824a0d0515f527b853771208ef1ed3feb281fc3da3532de3b20deafd145e30c3de979917e96c22f6c3113a731379eeb3200\",\"heir\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"secret\":\"4444444444444444444444444444444444444444444444444444444444444444\"}"
76
82
  },
77
83
  {
78
84
  "name": "a sealed knock: the first ask under a heir, by the heir, announcing her own key, to the heir",
@@ -80,7 +86,7 @@
80
86
  "ownSecret": "5555555555555555555555555555555555555555555555555555555555555555",
81
87
  "ephemeralSeed": "6666666666666666666666666666666666666666666666666666666666666666",
82
88
  "payload": "{\"to\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"by\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"next\":\"c6822637c7d310ec57627be00ba259d253749f4aaf644470cffbe53a35f73242\",\"seq\":1,\"time\":30000,\"method\":\"hello\",\"args\":{\"name\":\"quo\"}}",
83
- "bytes": "219e4d800da968d2a5fcb009c784f4746c7138edb9ee4844b739e830b05cf424263fd9388d350e13808f15e9297f753dd517db9f8eeca10de0708fd81a3b3c1b5444da304de91ae98fa4b94a62dae0673bdc0a7c6d97e445f8e08c5c3094b9facda3fa828caced7992ed8989f85a05dbe67da08c06b47b4e56174fe60e8169fc56c78d0966281c47c9b5300cca473e57d43e954b9d424ceb5a49eba9a4a79cd8b5973d21cdeb70f35b988314f68dcb5736003eb1a56c9be4087005fb5ae7d719eb39860ffb4220871161b76395054ab09ee5a323b6eea9a01f31d436b2a12983d5c32d887377e9d9ea845ecd4690660336e5e3e34d1c46f7e648e5a0d81f7a28ae2a52cc3021d10fe29de5c67b9f487b42f34a3e0149856ce8ee8cc8a90860daf624e0ffc758cd3600f069218abe5abdaa3041783add308fa4110899d216831e0016f5357a194344eb3551e48f0cb70f86b51fa68815c51660615034d2abdf3078c649bcca47fd769001b31928108e3ec9f0c1108e6ceec9a32c5ddd542c4543a1b8260936b3e5"
89
+ "bytes": "219e4d800da968d2a5fcb009c784f4746c7138edb9ee4844b739e830b05cf424e0590708caeb7c3c01e2ae8a2fa26a18bc504b84e06e80b104eab60c233b6ab42709b03434573a932c55e1977311a6358713251329e652d3d4bf212e4fe2062629f886a825315d8338de702e40457e2371eb4cd3b6633e0e986ea724fe6128a664bc61c7113106858540f20df784583e3ede4f125841a5d359cc94784bcee04d303b26b1008392420ae8f0930650c6aeefa3ed6138ab791a92870cb4f4d2d8e634e216cfdec86e8b3ceeef0b4f8a49477e95c04423808150ef381f7ef5390f3182f825a7be03e48edd981d66c3131ed9cca37c6348ef7f8dccc01018ff8417e09c6958b77ea269c58a47683f2b369ca13a434117d38f2f726b8a422e5004db32613ddc300d6f90805597d96ed51ef22bdd6353507857ab50edc2a3719e9081491a4722728275e263c9f6c286a1b39ab7cbd755805f2610410073ece392af0b3b960e68b0143a05674398927a57e2d99396b577ef7d1c618d32c9af619a79954ad413d7b5fe4cf3"
84
90
  }
85
91
  ]
86
92
  }
package/vectors/wire.json CHANGED
@@ -7,9 +7,9 @@
7
7
  {
8
8
  "name": "an ask frame: kind, id, pk, bytes",
9
9
  "id": 1,
10
- "pk": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c97787377b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13",
10
+ "pk": "6d773f8156c80eb9f34d0734e889d824a0d0515f527b853771208ef1ed3feb281fc3da3532de3b20deafd145e30c3de979917e96c22f6c3113a731379eeb3200",
11
11
  "bytes": "010203",
12
- "frame": "0000000001d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c97787377b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13010203"
12
+ "frame": "00000000016d773f8156c80eb9f34d0734e889d824a0d0515f527b853771208ef1ed3feb281fc3da3532de3b20deafd145e30c3de979917e96c22f6c3113a731379eeb3200010203"
13
13
  },
14
14
  {
15
15
  "name": "a reply frame: kind, id, bytes",
@@ -25,9 +25,9 @@
25
25
  {
26
26
  "name": "the announce frame, text",
27
27
  "pks": [
28
- "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c97787377b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13"
28
+ "6d773f8156c80eb9f34d0734e889d824a0d0515f527b853771208ef1ed3feb281fc3da3532de3b20deafd145e30c3de979917e96c22f6c3113a731379eeb3200"
29
29
  ],
30
- "frame": "{\"announce\":[\"d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c97787377b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13\"],\"suite\":1}",
30
+ "frame": "{\"announce\":[\"6d773f8156c80eb9f34d0734e889d824a0d0515f527b853771208ef1ed3feb281fc3da3532de3b20deafd145e30c3de979917e96c22f6c3113a731379eeb3200\"],\"suite\":1}",
31
31
  "suite": 1
32
32
  },
33
33
  {