@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
@@ -17,16 +17,31 @@
17
17
  // that arrive from the wire go to an own door or a held socket and never
18
18
  // onward by request or by fallback; that one rule is the rendezvous.
19
19
  import { Ward } from '../ward/ward.ts';
20
- import type { Ground, WardPointers } from '../ward/ground.ts';
20
+ import { maker, entropy as random, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
21
21
  import type { BeingClass, BeingLike } from '../being/types.ts';
22
22
  import { silence } from '../being/silence.ts';
23
23
  import { request, type Reach } from './reach.ts';
24
- import { openReply, wardSignPk } from '../ward/seal.ts';
24
+ import { isWardPk, openReply, wardSignPk } from '../ward/seal.ts';
25
25
  import { concat, sealingPair, KEY } from '../ward/arithmetic.ts';
26
26
  import { within, LATE } from '../ward/allowance.ts';
27
27
  import type { Kept, Store, WardRecord } from './store.ts';
28
28
 
29
- export type Hosted = WardPointers & { pk: string; name: string; record: WardRecord; partition: Record<string, unknown>; being(key: string): BeingLike | undefined; save(): Promise<void> };
29
+ // What a harbor hands its own device for one ward it serves. The two
30
+ // pointers, and beside them what the device needs to be a device: the pk it
31
+ // routes by, the name it is kept under, the record, a save the caller may
32
+ // wait for.
33
+ //
34
+ // And the beings the harbor made. `instantiate` is in the ground, so the
35
+ // harbor is the one that constructs every being of every ward it serves: it
36
+ // holds those objects because it made them, and `being` and `keys` say what
37
+ // it made. This is not a third path to a being. It is the device's own code
38
+ // reaching its own object in its own process, the same reach a being has on
39
+ // one she booted herself, and nothing of it crosses an edge. What a harbor
40
+ // never does is read the partition to decide anything, and the partition is
41
+ // not here: it holds every seed the ward has, and a side that wants to know
42
+ // which beings there are or which one is public asks the ward, which is
43
+ // what the ask pointer is for.
44
+ export type Hosted = WardPointers & { pk: string; name: string; record: WardRecord; being(key: string): BeingLike | undefined; keys(): string[]; save(): Promise<void> };
30
45
 
31
46
  // One ward's saves, in a line: the ward says it wrote, the harbor writes the
32
47
  // partition once the line is free, and never twice at once, so an older
@@ -42,6 +57,9 @@ export type Bound = { reach: Reach; held: boolean };
42
57
  export type Loader = (record: WardRecord) => Promise<Record<string, BeingClass>>;
43
58
 
44
59
  export const DEFAULT_CODE = 'classes/index.ts';
60
+ // The device's entropy, spelled once. A harbor draws it for a ward's seed at
61
+ // birth, for the lid of every probe it sends, and it hands the same function
62
+ // to every ward it boots as the ground's `random`.
45
63
  // How long a probe waits for the door behind a claim. A claim that answers
46
64
  // nothing in this time is not bound; the next announce is another chance.
47
65
  const PROBE = 10_000;
@@ -104,6 +122,18 @@ export class Harbor {
104
122
  for (const [pk, url] of Object.entries(await this.store.hints())) this.hint(pk, url);
105
123
  }
106
124
 
125
+ // The object a ward writes into, by the name it is kept under: the
126
+ // partition the harbor was handed at boot and keeps so it can save it.
127
+ // Nothing in the harbor reads it, and it is not on `Hosted`, because a
128
+ // side that holds a ward would then hold every seed in it and reach any
129
+ // being past every door. It is here for one reader, the conformance
130
+ // suite, whose census is ward state read straight and whose forgeries are
131
+ // ward state written straight, the way `MemoryHarbor.partitions` is. A
132
+ // side asks the ward.
133
+ partitionOf(name: string): Record<string, unknown> | undefined {
134
+ return this.#saving.get(name)?.memory;
135
+ }
136
+
107
137
  // ---- saving
108
138
 
109
139
  // The ward wrote. Its partition is saved once the line is free, and once
@@ -191,9 +221,8 @@ export class Harbor {
191
221
  // the one it sealed itself, and it reads nothing from it but that it is
192
222
  // the silence a door owes such a box, signed by the claimed key.
193
223
  async prove(pk: string, reach: Reach): Promise<boolean> {
194
- if (!/^[0-9a-f]{128}$/.test(pk)) return false;
224
+ if (!isWardPk(pk)) return false;
195
225
  try {
196
- const random = (n: number) => globalThis.crypto.getRandomValues(new Uint8Array(n));
197
226
  const lid = await sealingPair(random(KEY));
198
227
  const back = await within(PROBE, reach.carry(pk, concat([lid.pk, random(KEY)])));
199
228
  if (back === LATE || back === undefined) return false;
@@ -222,8 +251,18 @@ export class Harbor {
222
251
  for (const [pk, b] of this.reaches) if (b.reach === reach && !pks.includes(pk)) this.reaches.delete(pk);
223
252
  const claimed = [...new Set(pks)].slice(0, ANNOUNCE);
224
253
  const prove = async (pk: string) => {
225
- if (this.doors.has(pk) || !(await this.prove(pk, reach))) return;
254
+ if (this.doors.has(pk)) return;
255
+ // A line already held for this pk is not displaced by another line that
256
+ // proves the same pk. A proof says only that the ward is reachable that
257
+ // way, and a relay that forwards to the real ward proves as well as the
258
+ // ward's own line does: taking the binding would add a hop the relay
259
+ // chooses, and can drop. The first held line keeps it until it closes,
260
+ // which unbind says.
261
+ const standing = this.reaches.get(pk);
262
+ if (standing?.held && standing.reach !== reach) return;
263
+ if (!(await this.prove(pk, reach))) return;
226
264
  if (announces.get(reach) !== said) return; // a newer announce has spoken since
265
+ if (this.reaches.get(pk)?.held && this.reaches.get(pk)!.reach !== reach) return; // one took it while this was proving
227
266
  this.reaches.set(pk, { reach, held });
228
267
  };
229
268
  for (let i = 0; i < claimed.length; i += PROVING) await Promise.all(claimed.slice(i, i + PROVING).map(prove));
@@ -236,7 +275,7 @@ export class Harbor {
236
275
 
237
276
  // Mint a seed and boot an empty ward under a name: a world born here.
238
277
  async create(name: string, user = '', code = DEFAULT_CODE): Promise<Hosted> {
239
- const kept: Kept = { seed: globalThis.crypto.getRandomValues(new Uint8Array(32)), partition: {}, record: { pk: '', code, user } };
278
+ const kept: Kept = { seed: random(KEY), partition: {}, record: { pk: '', code, user } };
240
279
  await this.store.put(name, kept);
241
280
  const hosted = await this.host(name, kept);
242
281
  await this.store.record(name, hosted.record);
@@ -251,8 +290,12 @@ export class Harbor {
251
290
  const h = this.wards.get(name);
252
291
  if (h) {
253
292
  await h.save();
293
+ // Gone for whoever still holds the old pointers, whose save closure has
294
+ // this row in hand, and out of the map, which now says what this harbor
295
+ // serves and nothing else.
254
296
  const s = this.#saving.get(name);
255
297
  if (s) s.gone = true;
298
+ this.#saving.delete(name);
256
299
  this.wards.delete(name);
257
300
  this.doors.delete(h.pk);
258
301
  this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
@@ -260,10 +303,21 @@ export class Harbor {
260
303
  return this.store.take(name);
261
304
  }
262
305
 
263
- // Put what another harbor dropped, and boot it: same seed, same pk.
306
+ // Put what another harbor dropped, and boot it: same seed, same pk. A ward
307
+ // that will not boot is not adopted. This is the one path where a partition
308
+ // written somewhere else arrives, so it is the one place the ward's own
309
+ // shape check is met, and the name is refused for as long as it is kept:
310
+ // a partition put and left would hold the name against every later try,
311
+ // with a copy of it in a store that can never serve it. It goes back out,
312
+ // and the only copy is the one the caller is still holding.
264
313
  async adopt(name: string, kept: Kept): Promise<Hosted> {
265
314
  await this.store.put(name, kept);
266
- return this.host(name, kept);
315
+ try {
316
+ return await this.host(name, kept);
317
+ } catch (e) {
318
+ await this.store.take(name);
319
+ throw e;
320
+ }
267
321
  }
268
322
 
269
323
  // A ward from what the store keeps: the ground, once, and two pointers.
@@ -274,33 +328,47 @@ export class Harbor {
274
328
  const ground: Ground = {
275
329
  seed,
276
330
  memory,
277
- instantiate: (className, stance) => {
278
- // Own keys only: `constructor` is a name Object lends every registry.
279
- const C = Object.hasOwn(classes, className) ? classes[className] : Object.hasOwn(this.classes, className) ? this.classes[className] : undefined;
280
- if (!C) return null;
281
- const obj = new C(stance);
282
- objects.set(stance.cells, obj);
283
- return obj;
284
- },
331
+ // This ward's own classes first, then the harbor's: a ward that names a
332
+ // class of its own is answered with hers.
333
+ instantiate: maker(objects, classes, this.classes),
285
334
  carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
286
- random: (n) => globalThis.crypto.getRandomValues(new Uint8Array(n)),
335
+ random,
287
336
  wrote: () => this.#wrote(name),
288
337
  };
289
338
  const saving: Saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
290
339
  this.#saving.set(name, saving);
291
- const w = await Ward(ground);
340
+ // A ward that will not be born leaves nothing behind: a partition of a
341
+ // shape this kit cannot read throws here, and a name the harbor does not
342
+ // serve must not have a saving row saying it does. Learning the pk is
343
+ // inside this, because a ward that will not say its pk is a ward this
344
+ // harbor cannot route to and is no more born than one that threw.
345
+ let w: WardPointers, pk: string;
346
+ try {
347
+ w = await Ward(ground);
348
+ pk = await learnPk(w);
349
+ } catch (e) {
350
+ saving.gone = true;
351
+ this.#saving.delete(name);
352
+ throw e;
353
+ }
292
354
  // A save the caller may wait for: what the ward wrote so far is in the
293
355
  // store when this resolves, or the fault is counted. After a call through
294
356
  // a pointer the store is current, so a restart right after it finds
295
- // everything; a refusal at the door wrote nothing and saves nothing.
357
+ // everything; a refusal at the door wrote nothing and saves nothing, and
358
+ // neither does a read. A ward that wrote nothing has nothing to save, so
359
+ // asking it what it holds is free, which is what makes the ask pointer
360
+ // the way to find out rather than an expensive way.
296
361
  const save = async () => {
297
362
  if (saving.gone) return;
298
- saving.dirty = true;
299
363
  await this.#flush(name);
300
364
  };
301
365
  const door = async (bytes: Uint8Array) => {
302
366
  const r = await w.door(bytes);
303
- if (r.heard) await save();
367
+ // The reply waits for the store, so a restart right after it finds
368
+ // what the arrival wrote. Whether a key this door holds spoke is not
369
+ // the test: a stranger at the public being is not heard and her being
370
+ // may still have written, and a refusal wrote nothing whoever made it.
371
+ await save();
304
372
  return r;
305
373
  };
306
374
  const ask = async (method?: string, args?: Record<string, unknown>) => {
@@ -309,13 +377,18 @@ export class Harbor {
309
377
  await save();
310
378
  return out;
311
379
  };
312
- const bp = (await w.ask()) as { notes: { pk: string } }; // learned by asking, as anyone learns anything
380
+ // The objects map is keyed by the cells the ward handed instantiate, so
381
+ // the way back to a being is her cells; the partition is read for that
382
+ // one lookup and for nothing else, and a key with no object is a being
383
+ // who is not here this run. `keys` is what the map holds, so a caller
384
+ // never needs the beings table to find out what to ask for.
313
385
  const beings = memory.beings as Record<string, object> | undefined;
314
386
  const being = (key: string) => {
315
- const cells = beings?.[key];
387
+ const cells = beings && Object.hasOwn(beings, key) ? beings[key] : undefined;
316
388
  return cells ? objects.get(cells) : undefined;
317
389
  };
318
- const hosted: Hosted = { door, ask, pk: bp.notes.pk, name, record: { ...kept.record, pk: bp.notes.pk }, partition: memory, being, save };
390
+ const keys = () => Object.keys(beings ?? {}).filter((k) => being(k) !== undefined);
391
+ const hosted: Hosted = { door, ask, pk, name, record: { ...kept.record, pk }, being, keys, save };
319
392
  this.wards.set(name, hosted);
320
393
  this.doors.set(hosted.pk, door);
321
394
  await save();
@@ -12,11 +12,22 @@ import type { Harbor } from './core.ts';
12
12
  import { REFUSED, Socket, type Line } from './reach.ts';
13
13
 
14
14
  // `wake` is the device saying it is back: a phone out of a pocket, a
15
- // laptop's lid opened. A device that slept comes back with its timer
16
- // frozen and, often, its socket closed under it by the system without a
17
- // word. So a line in hand is made to speak, an announce, and a dead one
18
- // closes under the word and is dialed again; a line that is down is dialed
19
- // now instead of waiting out the backoff; a dial in flight is left alone.
15
+ // laptop's lid opened. A device that slept comes back with its timer frozen
16
+ // and, often, its socket closed under it by the system without a word. So
17
+ // nothing in hand is trusted: whatever line there is, open or still opening,
18
+ // is put down and a new one is dialed now.
19
+ //
20
+ // Nothing here can tell a line the system dropped from a live one, and
21
+ // asking is worse than dialing. An announce written into a dead line is
22
+ // taken into the send buffer and the error comes when TCP gives up
23
+ // retransmitting, one to two minutes on a Mac and up to fifteen on Linux,
24
+ // and for all of it every ask to a pk bound behind that line hears `late`.
25
+ // A probe with a bound would want a round trip the wire does not have: four
26
+ // words, none of them a ping, and making one out of an ask would give a
27
+ // second kit's listener a meaning to reproduce. Dialing costs one handshake
28
+ // when the line was fine, and an ask made meanwhile waits on the new socket
29
+ // rather than being lost, because a socket is a fallback from the moment it
30
+ // is dialed.
20
31
  export type Dialer = { url: string; socket: Socket | null; wake(): void; close(): void };
21
32
 
22
33
  export function dial(harbor: Harbor, url: string): Dialer {
@@ -24,7 +35,8 @@ export function dial(harbor: Harbor, url: string): Dialer {
24
35
  let stopped = false,
25
36
  wait = 1000,
26
37
  timer: ReturnType<typeof setTimeout> | undefined,
27
- line: Line | undefined; // the line in hand, open or still opening: close() closes it either way
38
+ line: Line | undefined, // the line in hand, open or still opening: close() closes it either way
39
+ held: { socket: Socket; shed: () => void } | undefined; // the one this dialer owns now, and how it gives back what the line took
28
40
  const connect = () => {
29
41
  timer = undefined; // no wait pending: this is the dial it was waiting for
30
42
  if (stopped) return;
@@ -41,11 +53,14 @@ export function dial(harbor: Harbor, url: string): Dialer {
41
53
  void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
42
54
  },
43
55
  (why) => {
44
- harbor.announcers.delete(say);
45
- harbor.unbind(s);
46
- if (d.socket === s) d.socket = null;
47
- const at = harbor.fallbacks.indexOf(s);
48
- if (at !== -1) harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
56
+ shed();
57
+ // A line this dialer already put down. Its close arrives whenever the
58
+ // system gets round to noticing, which on a socket that died under a
59
+ // sleeping device is minutes; by then the line dialed in its place is
60
+ // the one in hand, so this one gives back what it took and dials
61
+ // nothing. Only the line the dialer still holds dials again.
62
+ if (held?.socket !== s) return;
63
+ held = undefined;
49
64
  // A suite this listener will not speak is not a line that dropped. It
50
65
  // will not become speakable by asking again sooner, so the wait goes
51
66
  // straight to the ceiling and stays there until it changes its mind.
@@ -54,6 +69,16 @@ export function dial(harbor: Harbor, url: string): Dialer {
54
69
  wait = Math.min(wait * 2, 30000);
55
70
  },
56
71
  );
72
+ // Everything this line took from the harbor, given back. Said when it
73
+ // closes, and when the dialer puts it down without waiting for that.
74
+ const shed = () => {
75
+ harbor.announcers.delete(say);
76
+ harbor.unbind(s);
77
+ if (d.socket === s) d.socket = null;
78
+ const at = harbor.fallbacks.indexOf(s);
79
+ if (at !== -1) harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
80
+ };
81
+ held = { socket: s, shed };
57
82
  harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
58
83
  // What this harbor holds, said when the line opens and again whenever it
59
84
  // changes. A ward booted or adopted after the handshake is announced on
@@ -68,11 +93,13 @@ export function dial(harbor: Harbor, url: string): Dialer {
68
93
  };
69
94
  d.wake = () => {
70
95
  if (stopped) return;
71
- if (d.socket) return d.socket.announce([...harbor.doors.keys()]);
72
- if (timer === undefined) return;
73
96
  clearTimeout(timer);
74
97
  timer = undefined;
75
- wait = 1000;
98
+ wait = 1000; // the device is back, and the wait it froze belongs to the sleep, not to this line
99
+ const old = held;
100
+ held = undefined; // put down before the new one is dialed, so a fallback list never holds the dead line ahead of the live one
101
+ old?.shed();
102
+ old?.socket.close();
76
103
  connect();
77
104
  };
78
105
  d.close = () => {
@@ -9,3 +9,6 @@ export { dial, type Dialer } from './dial.ts';
9
9
  export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
10
10
  export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
11
11
  export type { Ground, WardPointers } from '../ward/ground.ts';
12
+ // What a harbor builds a ground out of. Convenience, never contract: a kit
13
+ // writing its own harbor may write these three again.
14
+ export { maker, entropy, learnPk } from '../ward/ground.ts';
@@ -4,8 +4,8 @@
4
4
  // keeping TWO pointers. Routes one ward pk to one door. Knows no being,
5
5
  // reads no partition, opens no byte. Several memory harbors may be linked,
6
6
  // which stands in for a wire, and cut, which stands in for weather.
7
- import type { BeingClass, Stance } from '../being/types.ts';
8
- import type { Ground, WardPointers } from '../ward/ground.ts';
7
+ import type { BeingClass, BeingLike } from '../being/types.ts';
8
+ import { maker, entropy, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
9
9
  import { hex } from '../ward/arithmetic.ts';
10
10
 
11
11
  export type Booted = WardPointers & { pk: string };
@@ -20,7 +20,7 @@ export class MemoryHarbor {
20
20
  cut = false;
21
21
  readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
22
22
  readonly wards = new Map<string, Booted>(); // seed -> pointers
23
- readonly objects = new WeakMap<object, unknown>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
23
+ readonly objects = new WeakMap<object, BeingLike>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
24
24
 
25
25
  // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
26
26
  async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
@@ -54,25 +54,19 @@ export class MemoryHarbor {
54
54
  const ground: Ground = {
55
55
  seed,
56
56
  memory,
57
- instantiate: (name: string, stance: Stance) => {
58
- // Own keys only: `constructor` is a name Object lends every registry.
59
- const C = Object.hasOwn(classes, name) ? classes[name] : undefined;
60
- if (!C) return null;
61
- const obj = new C(stance);
62
- this.objects.set(stance.cells, obj);
63
- return obj;
64
- },
57
+ // One objects map for the whole harbor, not one per ward: every ward
58
+ // here is in this process and a test reaches any being of any of them.
59
+ instantiate: maker(this.objects, classes),
65
60
  // bytes cross, copied, never a reference. both ways.
66
61
  carry: async (farPk, bytes) => {
67
62
  this.wire.push(hex(bytes));
68
63
  const back = await this.route(farPk, new Uint8Array(bytes));
69
64
  return back === undefined ? undefined : new Uint8Array(back);
70
65
  },
71
- random: (n) => globalThis.crypto.getRandomValues(new Uint8Array(n)),
66
+ random: entropy,
72
67
  };
73
68
  const w = await Ward(ground);
74
- const bp = (await w.ask()) as { notes: { pk: string } }; // the harbor learns its ward's pk the way anyone learns anything: by asking
75
- const booted: Booted = { ...w, pk: bp.notes.pk };
69
+ const booted: Booted = { ...w, pk: await learnPk(w) };
76
70
  this.doors.set(booted.pk, booted.door);
77
71
  this.wards.set(seed, booted);
78
72
  return booted;
@@ -19,7 +19,7 @@
19
19
  // at the far end, a connection that would not open, a socket that is gone.
20
20
  // A reach that sent the bytes and lost the line afterwards answers nothing
21
21
  // at all, and the ward's own bound ends the ask.
22
- import { hex, unhex } from '../ward/arithmetic.ts';
22
+ import { concat, hex, unhex } from '../ward/arithmetic.ts';
23
23
 
24
24
  export type Carry = (pk: string, bytes: Uint8Array) => Promise<Uint8Array | undefined>;
25
25
  export type Reach = { carry: Carry; close(): void };
@@ -33,8 +33,14 @@ export function request(url: string): Reach {
33
33
  close: () => {},
34
34
  carry: async (pk, bytes) => {
35
35
  let res: Response;
36
+ // A body of its own, not the caller's view: fetch reads it after this
37
+ // line returns, and a buffer somebody else may write is not a body. The
38
+ // copy is its own ArrayBuffer, which is what a body is; the bytes handed
39
+ // in are typed as backed by anything, shared memory included.
40
+ const body = new Uint8Array(bytes.length);
41
+ body.set(bytes);
36
42
  try {
37
- res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body: new Uint8Array(bytes) as unknown as BodyInit });
43
+ res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body });
38
44
  } catch {
39
45
  return undefined; // the connection never opened: nothing was delivered
40
46
  }
@@ -163,7 +169,7 @@ export class Socket implements Reach {
163
169
  const head = new Uint8Array(5);
164
170
  head[0] = back === undefined ? NONE : REPLY;
165
171
  new DataView(head.buffer).setUint32(1, id);
166
- if (line.readyState === OPEN) line.send(back === undefined ? head : concat(head, back));
172
+ if (line.readyState === OPEN) line.send(back === undefined ? head : concat([head, back]));
167
173
  });
168
174
  return;
169
175
  }
@@ -199,7 +205,7 @@ export class Socket implements Reach {
199
205
  return new Promise((ok) => {
200
206
  if (this.pending.size >= PENDING) this.pending.delete(this.pending.keys().next().value!); // the oldest, which the ward's bound ended long ago
201
207
  this.pending.set(id, ok);
202
- this.line.send(concat(head, bytes));
208
+ this.line.send(concat([head, bytes]));
203
209
  });
204
210
  }
205
211
 
@@ -207,10 +213,3 @@ export class Socket implements Reach {
207
213
  this.line.close();
208
214
  }
209
215
  }
210
-
211
- function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
212
- const out = new Uint8Array(a.length + b.length);
213
- out.set(a, 0);
214
- out.set(b, a.length);
215
- return out;
216
- }
@@ -43,8 +43,13 @@ export function allow(wanted: Wanted | undefined, ceiling: Allowance = CEILING,
43
43
  return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
44
44
  }
45
45
 
46
- // Whether a budget has anything left to spend. A door reads this on arrival.
47
- export const spent = (a: Allowance): boolean => !(a.time > 0);
46
+ // Whether a budget has anything left to spend, and the one reader of it: the
47
+ // door calls this on arrival and nothing else asks the question anywhere. A
48
+ // time that is not a whole number above zero is not a budget that ran out, it
49
+ // is no budget at all, and the door refuses both the same way, since a peer
50
+ // learns nothing from the difference. `allow` never makes one, so only bytes
51
+ // off the road ever carry one.
52
+ export const spent = (a: Allowance): boolean => !(Number.isSafeInteger(a.time) && a.time > 0);
48
53
 
49
54
  // What a wait that ran out comes back as. Its own value, held by nobody
50
55
  // outside the ward, so no answer from any door can be mistaken for it.
@@ -23,7 +23,7 @@ export const SIGNATURE = 64;
23
23
  export const NONCE = 12;
24
24
  export const TAG = 16;
25
25
  const SEAL_INFO = new TextEncoder().encode('quo-seal');
26
- const SEAL_SALT = new Uint8Array(0);
26
+ const SALT = new Uint8Array(0);
27
27
 
28
28
  // A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
29
29
  const ED_SECRET = unhex('302e020100300506032b657004220420');
@@ -135,22 +135,31 @@ export async function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uin
135
135
  return shared;
136
136
  }
137
137
 
138
+ // HKDF-SHA-256: bytes from a secret, under a label that says what they are
139
+ // for. No salt, because the secret handed in is already a secret of full
140
+ // strength and the label is what keeps one use apart from another. Every
141
+ // label in this kit is spelled where it is used, never here: this is
142
+ // arithmetic and a label is a decision.
143
+ export async function derive(secret: Uint8Array, label: Uint8Array, bytes: number): Promise<Uint8Array> {
144
+ const material = await subtle().importKey('raw', secret as BufferSource, 'HKDF', false, ['deriveBits']);
145
+ return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: label as BufferSource }, material, bytes * 8));
146
+ }
147
+
138
148
  // One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
139
149
  // no randomness of its own: the key it pairs with is fresh on every message.
140
150
  async function cipherKey(shared: Uint8Array, use: KeyUsage) {
141
- const material = await subtle().importKey('raw', shared as BufferSource, 'HKDF', false, ['deriveBits']);
142
- const out = new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SEAL_SALT, info: SEAL_INFO }, material, (KEY + NONCE) * 8));
151
+ const out = await derive(shared, SEAL_INFO, KEY + NONCE);
143
152
  return { key: await subtle().importKey('raw', out.subarray(0, KEY) as BufferSource, 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
144
153
  }
145
154
  // The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
146
155
  export async function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
147
156
  const { key, nonce } = await cipherKey(shared, 'encrypt');
148
- return new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, plaintext as BufferSource));
157
+ return new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: nonce as BufferSource, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, plaintext as BufferSource));
149
158
  }
150
159
  export async function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
151
160
  if (ciphertext.length < TAG) throw new Error('short input');
152
161
  const { key, nonce } = await cipherKey(shared, 'decrypt');
153
- return new Uint8Array(await subtle().decrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, ciphertext as BufferSource));
162
+ return new Uint8Array(await subtle().decrypt({ name: 'AES-GCM', iv: nonce as BufferSource, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, ciphertext as BufferSource));
154
163
  }
155
164
 
156
165
  // A box: an ephemeral X25519 pk outside, one ciphertext sealed to the
package/src/ward/door.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  // asker rides back with the object.
10
10
  import { isSilence, isWord } from '../being/silence.ts';
11
11
  import { digest } from '../being/digest.ts';
12
- import type { Asker, BeingLike, DoorWord, JsonObject } from '../being/types.ts';
12
+ import type { Asker, BeingLike, DoorWord, JsonObject, OccupantRecord } from '../being/types.ts';
13
13
  import { at } from './partition.ts';
14
14
  import type { Heirs } from './heirs.ts';
15
15
  import { openAsk, sealReply, verifyAsk, type ReplyPayload, type WardKey } from './seal.ts';
@@ -17,7 +17,7 @@ import { spent } from './allowance.ts';
17
17
  import { KEY, sealingPair } from './arithmetic.ts';
18
18
  import { cellFault } from './cells.ts';
19
19
 
20
- export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, unknown> } };
20
+ export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, OccupantRecord> } };
21
21
  export type Judged = { bytes: Uint8Array; heard: boolean };
22
22
  const SILENCE: ReplyPayload = { silence: true };
23
23
  const said = (quo: DoorWord): ReplyPayload => ({ quo });
@@ -69,9 +69,10 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
69
69
  if (!a) return null; // D1. it did not open. there is nobody to answer.
70
70
  const { to, payload, ephemeralPk } = a;
71
71
  const refuse = (): Verdict => ({ reply: SILENCE, ephemeralPk, heard: false });
72
- // D2, the rest of it. The allowance is read before anything is done under
73
- // it, and hops at zero is refused so that a relay chain invented later
74
- // meets doors that already stop it. Nothing sets hops.
72
+ // D2. The allowance is read here, before anything is done under it, and
73
+ // here only: a time that is not a whole number above zero is malformed and
74
+ // is refused as one. Hops at zero is refused beside it, so that a relay
75
+ // chain invented later meets doors that already stop it. Nothing sets hops.
75
76
  if (spent({ time: payload.time }) || payload.hops === 0) return refuse();
76
77
  const args = payload.args ?? {};
77
78
  if (to === null) {
@@ -81,7 +82,10 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
81
82
  const pk = publicKey();
82
83
  const pub = pk !== null ? doors.get(pk) : undefined;
83
84
  if (!pub || !(await verifyAsk(a, payload.by))) return refuse();
84
- return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: true };
85
+ // She answers, and the bit stays false: no key this door holds spoke.
86
+ // The public being is the one place a stranger is answered by design,
87
+ // so it is the one place a harbor must still be able to rate her.
88
+ return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: false };
85
89
  }
86
90
  const h = heirs.admits(to, payload.by);
87
91
  if (!h) {
@@ -120,8 +124,13 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
120
124
  // door never throws: that reply is noise, a plain silence sealed to a key
121
125
  // nobody holds, and the same is written for anything else the seal will
122
126
  // not take.
127
+ // `judge` reads rows it does not check: open() reads the partition's shape
128
+ // at birth, so a row it acts on is the shape it expects. That is the first
129
+ // line and this is the second, because "the door never throws" is a promise
130
+ // to the harbor, which has nobody to hand a rejection to and would count it
131
+ // as no answer at all. Anything unforeseen is the silence a stranger hears.
123
132
  return async function door(bytes: Uint8Array): Promise<Judged> {
124
- const out = await judge(bytes);
133
+ const out = await judge(bytes).catch(() => null);
125
134
  const reply = out?.reply ?? SILENCE;
126
135
  const heard = out?.heard ?? false;
127
136
  try {
@@ -2,7 +2,7 @@
2
2
  // The ground. The one object a harbor passes a ward at birth. Six things,
3
3
  // never a seventh. Everything a runtime differs on arrives here, which is
4
4
  // why the ward itself knows no runtime.
5
- import type { Stance, BeingLike } from '../being/types.ts';
5
+ import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
6
6
 
7
7
  export type Ground = {
8
8
  seed: string | Uint8Array; // the ward derives its pk from it and nothing else
@@ -44,3 +44,45 @@ export type WardPointers = {
44
44
  door(bytes: Uint8Array): Promise<{ bytes: Uint8Array; heard: boolean }>;
45
45
  ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
46
46
  };
47
+
48
+ // ---- what every harbor builds a ground out of. Three pieces, because every
49
+ // harbor in this tree writes the same three and the spec keeps the harbors
50
+ // themselves apart: what a memory harbor and a real one differ on is the
51
+ // route and the store, and nothing here. A second kit writes its own harbor
52
+ // and may write these again; they are convenience, never contract.
53
+
54
+ // The code half of a ground, and the map back to what it made. A class is
55
+ // found by own key only, since `constructor` is a name Object lends every
56
+ // registry, and the first registry holding the name wins, so a ward's own
57
+ // classes stand in front of the harbor's. The object is remembered by the
58
+ // cells the ward handed in, which is how a side reaches a being it made and
59
+ // how that hand follows her out when she is unbooted.
60
+ export const maker =
61
+ (objects: WeakMap<object, BeingLike>, ...registries: (Record<string, BeingClass> | undefined)[]): Ground['instantiate'] =>
62
+ (className: string, stance: Stance) => {
63
+ const r = registries.find((reg) => reg !== undefined && Object.hasOwn(reg, className));
64
+ const C = r?.[className];
65
+ if (!C) return null;
66
+ const obj = new C(stance);
67
+ objects.set(stance.cells, obj);
68
+ return obj;
69
+ };
70
+
71
+ // Entropy, from the one place every terrain that runs Quo has it. Every key a
72
+ // ward mints is drawn from this, so a harbor that wants another source hands
73
+ // its own and nothing here has to know.
74
+ export const entropy = (n: number): Uint8Array => globalThis.crypto.getRandomValues(new Uint8Array(n));
75
+
76
+ // A ward's pk, learned the way anyone learns anything: by asking. The empty
77
+ // ask on the ask pointer is the ward's own describe and its notes carry the
78
+ // pk. A harbor has no other way to it and wants none: the ward mints it from
79
+ // the seed, and a harbor that read it off the seed itself would be a second
80
+ // derivation to keep in step with the first. A ward that answers anything
81
+ // else is not one this harbor can route to, and says so here rather than
82
+ // leaving an undefined pk in a directory.
83
+ export async function learnPk(w: WardPointers): Promise<string> {
84
+ const notes = (await w.ask()) as { notes?: { pk?: unknown } } | null;
85
+ const pk = notes?.notes?.pk;
86
+ if (typeof pk !== 'string') throw new Error('the ward did not say its pk');
87
+ return pk;
88
+ }