@quo-systems/quo 0.2.10 → 0.2.11

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,12 +17,16 @@
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.js';
20
+ import { maker, entropy as random, learnPk } from '../ward/ground.js';
20
21
  import { silence } from '../being/silence.js';
21
22
  import { request } from './reach.js';
22
- import { openReply, wardSignPk } from '../ward/seal.js';
23
+ import { isWardPk, openReply, wardSignPk } from '../ward/seal.js';
23
24
  import { concat, sealingPair, KEY } from '../ward/arithmetic.js';
24
25
  import { within, LATE } from '../ward/allowance.js';
25
26
  export const DEFAULT_CODE = 'classes/index.ts';
27
+ // The device's entropy, spelled once. A harbor draws it for a ward's seed at
28
+ // birth, for the lid of every probe it sends, and it hands the same function
29
+ // to every ward it boots as the ground's `random`.
26
30
  // How long a probe waits for the door behind a claim. A claim that answers
27
31
  // nothing in this time is not bound; the next announce is another chance.
28
32
  const PROBE = 10_000;
@@ -85,6 +89,17 @@ export class Harbor {
85
89
  for (const [pk, url] of Object.entries(await this.store.hints()))
86
90
  this.hint(pk, url);
87
91
  }
92
+ // The object a ward writes into, by the name it is kept under: the
93
+ // partition the harbor was handed at boot and keeps so it can save it.
94
+ // Nothing in the harbor reads it, and it is not on `Hosted`, because a
95
+ // side that holds a ward would then hold every seed in it and reach any
96
+ // being past every door. It is here for one reader, the conformance
97
+ // suite, whose census is ward state read straight and whose forgeries are
98
+ // ward state written straight, the way `MemoryHarbor.partitions` is. A
99
+ // side asks the ward.
100
+ partitionOf(name) {
101
+ return this.#saving.get(name)?.memory;
102
+ }
88
103
  // ---- saving
89
104
  // The ward wrote. Its partition is saved once the line is free, and once
90
105
  // for every burst of writes, in the order the writes came.
@@ -178,10 +193,9 @@ export class Harbor {
178
193
  // the one it sealed itself, and it reads nothing from it but that it is
179
194
  // the silence a door owes such a box, signed by the claimed key.
180
195
  async prove(pk, reach) {
181
- if (!/^[0-9a-f]{128}$/.test(pk))
196
+ if (!isWardPk(pk))
182
197
  return false;
183
198
  try {
184
- const random = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
185
199
  const lid = await sealingPair(random(KEY));
186
200
  const back = await within(PROBE, reach.carry(pk, concat([lid.pk, random(KEY)])));
187
201
  if (back === LATE || back === undefined)
@@ -213,10 +227,23 @@ export class Harbor {
213
227
  this.reaches.delete(pk);
214
228
  const claimed = [...new Set(pks)].slice(0, ANNOUNCE);
215
229
  const prove = async (pk) => {
216
- if (this.doors.has(pk) || !(await this.prove(pk, reach)))
230
+ if (this.doors.has(pk))
231
+ return;
232
+ // A line already held for this pk is not displaced by another line that
233
+ // proves the same pk. A proof says only that the ward is reachable that
234
+ // way, and a relay that forwards to the real ward proves as well as the
235
+ // ward's own line does: taking the binding would add a hop the relay
236
+ // chooses, and can drop. The first held line keeps it until it closes,
237
+ // which unbind says.
238
+ const standing = this.reaches.get(pk);
239
+ if (standing?.held && standing.reach !== reach)
240
+ return;
241
+ if (!(await this.prove(pk, reach)))
217
242
  return;
218
243
  if (announces.get(reach) !== said)
219
244
  return; // a newer announce has spoken since
245
+ if (this.reaches.get(pk)?.held && this.reaches.get(pk).reach !== reach)
246
+ return; // one took it while this was proving
220
247
  this.reaches.set(pk, { reach, held });
221
248
  };
222
249
  for (let i = 0; i < claimed.length; i += PROVING)
@@ -230,7 +257,7 @@ export class Harbor {
230
257
  // ---- wards
231
258
  // Mint a seed and boot an empty ward under a name: a world born here.
232
259
  async create(name, user = '', code = DEFAULT_CODE) {
233
- const kept = { seed: globalThis.crypto.getRandomValues(new Uint8Array(32)), partition: {}, record: { pk: '', code, user } };
260
+ const kept = { seed: random(KEY), partition: {}, record: { pk: '', code, user } };
234
261
  await this.store.put(name, kept);
235
262
  const hosted = await this.host(name, kept);
236
263
  await this.store.record(name, hosted.record);
@@ -244,19 +271,35 @@ export class Harbor {
244
271
  const h = this.wards.get(name);
245
272
  if (h) {
246
273
  await h.save();
274
+ // Gone for whoever still holds the old pointers, whose save closure has
275
+ // this row in hand, and out of the map, which now says what this harbor
276
+ // serves and nothing else.
247
277
  const s = this.#saving.get(name);
248
278
  if (s)
249
279
  s.gone = true;
280
+ this.#saving.delete(name);
250
281
  this.wards.delete(name);
251
282
  this.doors.delete(h.pk);
252
283
  this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
253
284
  }
254
285
  return this.store.take(name);
255
286
  }
256
- // Put what another harbor dropped, and boot it: same seed, same pk.
287
+ // Put what another harbor dropped, and boot it: same seed, same pk. A ward
288
+ // that will not boot is not adopted. This is the one path where a partition
289
+ // written somewhere else arrives, so it is the one place the ward's own
290
+ // shape check is met, and the name is refused for as long as it is kept:
291
+ // a partition put and left would hold the name against every later try,
292
+ // with a copy of it in a store that can never serve it. It goes back out,
293
+ // and the only copy is the one the caller is still holding.
257
294
  async adopt(name, kept) {
258
295
  await this.store.put(name, kept);
259
- return this.host(name, kept);
296
+ try {
297
+ return await this.host(name, kept);
298
+ }
299
+ catch (e) {
300
+ await this.store.take(name);
301
+ throw e;
302
+ }
260
303
  }
261
304
  // A ward from what the store keeps: the ground, once, and two pointers.
262
305
  async host(name, kept) {
@@ -266,36 +309,49 @@ export class Harbor {
266
309
  const ground = {
267
310
  seed,
268
311
  memory,
269
- instantiate: (className, stance) => {
270
- // Own keys only: `constructor` is a name Object lends every registry.
271
- const C = Object.hasOwn(classes, className) ? classes[className] : Object.hasOwn(this.classes, className) ? this.classes[className] : undefined;
272
- if (!C)
273
- return null;
274
- const obj = new C(stance);
275
- objects.set(stance.cells, obj);
276
- return obj;
277
- },
312
+ // This ward's own classes first, then the harbor's: a ward that names a
313
+ // class of its own is answered with hers.
314
+ instantiate: maker(objects, classes, this.classes),
278
315
  carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
279
- random: (n) => globalThis.crypto.getRandomValues(new Uint8Array(n)),
316
+ random,
280
317
  wrote: () => this.#wrote(name),
281
318
  };
282
319
  const saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
283
320
  this.#saving.set(name, saving);
284
- const w = await Ward(ground);
321
+ // A ward that will not be born leaves nothing behind: a partition of a
322
+ // shape this kit cannot read throws here, and a name the harbor does not
323
+ // serve must not have a saving row saying it does. Learning the pk is
324
+ // inside this, because a ward that will not say its pk is a ward this
325
+ // harbor cannot route to and is no more born than one that threw.
326
+ let w, pk;
327
+ try {
328
+ w = await Ward(ground);
329
+ pk = await learnPk(w);
330
+ }
331
+ catch (e) {
332
+ saving.gone = true;
333
+ this.#saving.delete(name);
334
+ throw e;
335
+ }
285
336
  // A save the caller may wait for: what the ward wrote so far is in the
286
337
  // store when this resolves, or the fault is counted. After a call through
287
338
  // a pointer the store is current, so a restart right after it finds
288
- // everything; a refusal at the door wrote nothing and saves nothing.
339
+ // everything; a refusal at the door wrote nothing and saves nothing, and
340
+ // neither does a read. A ward that wrote nothing has nothing to save, so
341
+ // asking it what it holds is free, which is what makes the ask pointer
342
+ // the way to find out rather than an expensive way.
289
343
  const save = async () => {
290
344
  if (saving.gone)
291
345
  return;
292
- saving.dirty = true;
293
346
  await this.#flush(name);
294
347
  };
295
348
  const door = async (bytes) => {
296
349
  const r = await w.door(bytes);
297
- if (r.heard)
298
- await save();
350
+ // The reply waits for the store, so a restart right after it finds
351
+ // what the arrival wrote. Whether a key this door holds spoke is not
352
+ // the test: a stranger at the public being is not heard and her being
353
+ // may still have written, and a refusal wrote nothing whoever made it.
354
+ await save();
299
355
  return r;
300
356
  };
301
357
  const ask = async (method, args) => {
@@ -305,13 +361,18 @@ export class Harbor {
305
361
  await save();
306
362
  return out;
307
363
  };
308
- const bp = (await w.ask()); // learned by asking, as anyone learns anything
364
+ // The objects map is keyed by the cells the ward handed instantiate, so
365
+ // the way back to a being is her cells; the partition is read for that
366
+ // one lookup and for nothing else, and a key with no object is a being
367
+ // who is not here this run. `keys` is what the map holds, so a caller
368
+ // never needs the beings table to find out what to ask for.
309
369
  const beings = memory.beings;
310
370
  const being = (key) => {
311
- const cells = beings?.[key];
371
+ const cells = beings && Object.hasOwn(beings, key) ? beings[key] : undefined;
312
372
  return cells ? objects.get(cells) : undefined;
313
373
  };
314
- const hosted = { door, ask, pk: bp.notes.pk, name, record: { ...kept.record, pk: bp.notes.pk }, partition: memory, being, save };
374
+ const keys = () => Object.keys(beings ?? {}).filter((k) => being(k) !== undefined);
375
+ const hosted = { door, ask, pk, name, record: { ...kept.record, pk }, being, keys, save };
315
376
  this.wards.set(name, hosted);
316
377
  this.doors.set(hosted.pk, door);
317
378
  await save();
@@ -1,7 +1,8 @@
1
1
  import { REFUSED, Socket } from './reach.js';
2
2
  export function dial(harbor, url) {
3
3
  const d = { url, socket: null, wake: () => { }, close: () => { } };
4
- let stopped = false, wait = 1000, timer, line; // the line in hand, open or still opening: close() closes it either way
4
+ let stopped = false, wait = 1000, timer, line, // the line in hand, open or still opening: close() closes it either way
5
+ held; // the one this dialer owns now, and how it gives back what the line took
5
6
  const connect = () => {
6
7
  timer = undefined; // no wait pending: this is the dial it was waiting for
7
8
  if (stopped)
@@ -15,13 +16,15 @@ export function dial(harbor, url) {
15
16
  wait = 1000;
16
17
  void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
17
18
  }, (why) => {
18
- harbor.announcers.delete(say);
19
- harbor.unbind(s);
20
- if (d.socket === s)
21
- d.socket = null;
22
- const at = harbor.fallbacks.indexOf(s);
23
- if (at !== -1)
24
- harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
19
+ shed();
20
+ // A line this dialer already put down. Its close arrives whenever the
21
+ // system gets round to noticing, which on a socket that died under a
22
+ // sleeping device is minutes; by then the line dialed in its place is
23
+ // the one in hand, so this one gives back what it took and dials
24
+ // nothing. Only the line the dialer still holds dials again.
25
+ if (held?.socket !== s)
26
+ return;
27
+ held = undefined;
25
28
  // A suite this listener will not speak is not a line that dropped. It
26
29
  // will not become speakable by asking again sooner, so the wait goes
27
30
  // straight to the ceiling and stays there until it changes its mind.
@@ -31,6 +34,18 @@ export function dial(harbor, url) {
31
34
  timer = setTimeout(connect, wait);
32
35
  wait = Math.min(wait * 2, 30000);
33
36
  });
37
+ // Everything this line took from the harbor, given back. Said when it
38
+ // closes, and when the dialer puts it down without waiting for that.
39
+ const shed = () => {
40
+ harbor.announcers.delete(say);
41
+ harbor.unbind(s);
42
+ if (d.socket === s)
43
+ d.socket = null;
44
+ const at = harbor.fallbacks.indexOf(s);
45
+ if (at !== -1)
46
+ harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
47
+ };
48
+ held = { socket: s, shed };
34
49
  harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
35
50
  // What this harbor holds, said when the line opens and again whenever it
36
51
  // changes. A ward booted or adopted after the handshake is announced on
@@ -47,13 +62,13 @@ export function dial(harbor, url) {
47
62
  d.wake = () => {
48
63
  if (stopped)
49
64
  return;
50
- if (d.socket)
51
- return d.socket.announce([...harbor.doors.keys()]);
52
- if (timer === undefined)
53
- return;
54
65
  clearTimeout(timer);
55
66
  timer = undefined;
56
- wait = 1000;
67
+ wait = 1000; // the device is back, and the wait it froze belongs to the sleep, not to this line
68
+ const old = held;
69
+ held = undefined; // put down before the new one is dialed, so a fallback list never holds the dead line ahead of the live one
70
+ old?.shed();
71
+ old?.socket.close();
57
72
  connect();
58
73
  };
59
74
  d.close = () => {
@@ -4,3 +4,4 @@ export { dial, type Dialer } from './dial.ts';
4
4
  export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
5
5
  export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
6
6
  export type { Ground, WardPointers } from '../ward/ground.ts';
7
+ export { maker, entropy, learnPk } from '../ward/ground.ts';
@@ -8,3 +8,6 @@ export { Harbor, DEFAULT_CODE } from './core.js';
8
8
  export { dial } from './dial.js';
9
9
  export { MemoryStore, values } from './store.js';
10
10
  export { request, Socket, SUITE, REFUSED } from './reach.js';
11
+ // What a harbor builds a ground out of. Convenience, never contract: a kit
12
+ // writing its own harbor may write these three again.
13
+ export { maker, entropy, learnPk } from '../ward/ground.js';
@@ -1,5 +1,5 @@
1
- import type { BeingClass } from '../being/types.ts';
2
- import type { Ground, WardPointers } from '../ward/ground.ts';
1
+ import type { BeingClass, BeingLike } from '../being/types.ts';
2
+ import { type Ground, type WardPointers } from '../ward/ground.ts';
3
3
  export type Booted = WardPointers & {
4
4
  pk: string;
5
5
  };
@@ -16,7 +16,7 @@ export declare class MemoryHarbor {
16
16
  cut: boolean;
17
17
  readonly partitions: Map<string, Record<string, unknown>>;
18
18
  readonly wards: Map<string, Booted>;
19
- readonly objects: WeakMap<object, unknown>;
19
+ readonly objects: WeakMap<object, BeingLike>;
20
20
  route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
21
21
  link(harbor: MemoryHarbor): void;
22
22
  boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted>;
@@ -1,3 +1,4 @@
1
+ import { maker, entropy, learnPk } from '../ward/ground.js';
1
2
  import { hex } from '../ward/arithmetic.js';
2
3
  export class MemoryHarbor {
3
4
  doors = new Map(); // ward pk -> door
@@ -42,26 +43,19 @@ export class MemoryHarbor {
42
43
  const ground = {
43
44
  seed,
44
45
  memory,
45
- instantiate: (name, stance) => {
46
- // Own keys only: `constructor` is a name Object lends every registry.
47
- const C = Object.hasOwn(classes, name) ? classes[name] : undefined;
48
- if (!C)
49
- return null;
50
- const obj = new C(stance);
51
- this.objects.set(stance.cells, obj);
52
- return obj;
53
- },
46
+ // One objects map for the whole harbor, not one per ward: every ward
47
+ // here is in this process and a test reaches any being of any of them.
48
+ instantiate: maker(this.objects, classes),
54
49
  // bytes cross, copied, never a reference. both ways.
55
50
  carry: async (farPk, bytes) => {
56
51
  this.wire.push(hex(bytes));
57
52
  const back = await this.route(farPk, new Uint8Array(bytes));
58
53
  return back === undefined ? undefined : new Uint8Array(back);
59
54
  },
60
- random: (n) => globalThis.crypto.getRandomValues(new Uint8Array(n)),
55
+ random: entropy,
61
56
  };
62
57
  const w = await Ward(ground);
63
- const bp = (await w.ask()); // the harbor learns its ward's pk the way anyone learns anything: by asking
64
- const booted = { ...w, pk: bp.notes.pk };
58
+ const booted = { ...w, pk: await learnPk(w) };
65
59
  this.doors.set(booted.pk, booted.door);
66
60
  this.wards.set(seed, booted);
67
61
  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.js';
22
+ import { concat, hex, unhex } from '../ward/arithmetic.js';
23
23
  const never = () => new Promise(() => { }); // sent, and no word since: the ward's bound decides
24
24
  // A request reach to one listener.
25
25
  export function request(url) {
@@ -28,8 +28,14 @@ export function request(url) {
28
28
  close: () => { },
29
29
  carry: async (pk, bytes) => {
30
30
  let res;
31
+ // A body of its own, not the caller's view: fetch reads it after this
32
+ // line returns, and a buffer somebody else may write is not a body. The
33
+ // copy is its own ArrayBuffer, which is what a body is; the bytes handed
34
+ // in are typed as backed by anything, shared memory included.
35
+ const body = new Uint8Array(bytes.length);
36
+ body.set(bytes);
31
37
  try {
32
- res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body: new Uint8Array(bytes) });
38
+ res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body });
33
39
  }
34
40
  catch {
35
41
  return undefined; // the connection never opened: nothing was delivered
@@ -137,7 +143,7 @@ export class Socket {
137
143
  head[0] = back === undefined ? NONE : REPLY;
138
144
  new DataView(head.buffer).setUint32(1, id);
139
145
  if (line.readyState === OPEN)
140
- line.send(back === undefined ? head : concat(head, back));
146
+ line.send(back === undefined ? head : concat([head, back]));
141
147
  });
142
148
  return;
143
149
  }
@@ -176,16 +182,10 @@ export class Socket {
176
182
  if (this.pending.size >= PENDING)
177
183
  this.pending.delete(this.pending.keys().next().value); // the oldest, which the ward's bound ended long ago
178
184
  this.pending.set(id, ok);
179
- this.line.send(concat(head, bytes));
185
+ this.line.send(concat([head, bytes]));
180
186
  });
181
187
  }
182
188
  close() {
183
189
  this.line.close();
184
190
  }
185
191
  }
186
- function concat(a, b) {
187
- const out = new Uint8Array(a.length + b.length);
188
- out.set(a, 0);
189
- out.set(b, a.length);
190
- return out;
191
- }
@@ -33,8 +33,13 @@ export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
33
33
  };
34
34
  return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
35
35
  }
36
- // Whether a budget has anything left to spend. A door reads this on arrival.
37
- export const spent = (a) => !(a.time > 0);
36
+ // Whether a budget has anything left to spend, and the one reader of it: the
37
+ // door calls this on arrival and nothing else asks the question anywhere. A
38
+ // time that is not a whole number above zero is not a budget that ran out, it
39
+ // is no budget at all, and the door refuses both the same way, since a peer
40
+ // learns nothing from the difference. `allow` never makes one, so only bytes
41
+ // off the road ever carry one.
42
+ export const spent = (a) => !(Number.isSafeInteger(a.time) && a.time > 0);
38
43
  // What a wait that ran out comes back as. Its own value, held by nobody
39
44
  // outside the ward, so no answer from any door can be mistaken for it.
40
45
  export const LATE = Symbol('late');
@@ -17,6 +17,7 @@ export declare function sealingPair(seed: Uint8Array): Promise<Pair>;
17
17
  export declare function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array>;
18
18
  export declare function verify(message: Uint8Array, signature: Uint8Array, pk: Uint8Array): Promise<boolean>;
19
19
  export declare function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uint8Array>;
20
+ export declare function derive(secret: Uint8Array, label: Uint8Array, bytes: number): Promise<Uint8Array>;
20
21
  export declare function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
21
22
  export declare function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
22
23
  export declare function box(inside: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{
@@ -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
  // A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
28
28
  const ED_SECRET = unhex('302e020100300506032b657004220420');
29
29
  const X_SECRET = unhex('302e020100300506032b656e04220420');
@@ -132,11 +132,19 @@ export async function agree(secret, peerPk) {
132
132
  throw new Error('dead agreement'); // a padlock that was not a real key
133
133
  return shared;
134
134
  }
135
+ // HKDF-SHA-256: bytes from a secret, under a label that says what they are
136
+ // for. No salt, because the secret handed in is already a secret of full
137
+ // strength and the label is what keeps one use apart from another. Every
138
+ // label in this kit is spelled where it is used, never here: this is
139
+ // arithmetic and a label is a decision.
140
+ export async function derive(secret, label, bytes) {
141
+ const material = await subtle().importKey('raw', secret, 'HKDF', false, ['deriveBits']);
142
+ return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: label }, material, bytes * 8));
143
+ }
135
144
  // One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
136
145
  // no randomness of its own: the key it pairs with is fresh on every message.
137
146
  async function cipherKey(shared, use) {
138
- const material = await subtle().importKey('raw', shared, 'HKDF', false, ['deriveBits']);
139
- const out = new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SEAL_SALT, info: SEAL_INFO }, material, (KEY + NONCE) * 8));
147
+ const out = await derive(shared, SEAL_INFO, KEY + NONCE);
140
148
  return { key: await subtle().importKey('raw', out.subarray(0, KEY), 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
141
149
  }
142
150
  // The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
@@ -1,11 +1,11 @@
1
- import type { Asker, BeingLike, JsonObject } from '../being/types.ts';
1
+ import type { Asker, BeingLike, JsonObject, OccupantRecord } from '../being/types.ts';
2
2
  import type { Heirs } from './heirs.ts';
3
3
  import { type ReplyPayload, type WardKey } from './seal.ts';
4
4
  export type Door = {
5
5
  key: string;
6
6
  being: BeingLike;
7
7
  cells: {
8
- occupants: Record<string, unknown>;
8
+ occupants: Record<string, OccupantRecord>;
9
9
  };
10
10
  };
11
11
  export type Judged = {
package/dist/ward/door.js CHANGED
@@ -66,9 +66,10 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
66
66
  return null; // D1. it did not open. there is nobody to answer.
67
67
  const { to, payload, ephemeralPk } = a;
68
68
  const refuse = () => ({ reply: SILENCE, ephemeralPk, heard: false });
69
- // D2, the rest of it. The allowance is read before anything is done under
70
- // it, and hops at zero is refused so that a relay chain invented later
71
- // meets doors that already stop it. Nothing sets hops.
69
+ // D2. The allowance is read here, before anything is done under it, and
70
+ // here only: a time that is not a whole number above zero is malformed and
71
+ // is refused as one. Hops at zero is refused beside it, so that a relay
72
+ // chain invented later meets doors that already stop it. Nothing sets hops.
72
73
  if (spent({ time: payload.time }) || payload.hops === 0)
73
74
  return refuse();
74
75
  const args = payload.args ?? {};
@@ -80,7 +81,10 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
80
81
  const pub = pk !== null ? doors.get(pk) : undefined;
81
82
  if (!pub || !(await verifyAsk(a, payload.by)))
82
83
  return refuse();
83
- return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: true };
84
+ // She answers, and the bit stays false: no key this door holds spoke.
85
+ // The public being is the one place a stranger is answered by design,
86
+ // so it is the one place a harbor must still be able to rate her.
87
+ return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: false };
84
88
  }
85
89
  const h = heirs.admits(to, payload.by);
86
90
  if (!h) {
@@ -124,8 +128,13 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
124
128
  // door never throws: that reply is noise, a plain silence sealed to a key
125
129
  // nobody holds, and the same is written for anything else the seal will
126
130
  // not take.
131
+ // `judge` reads rows it does not check: open() reads the partition's shape
132
+ // at birth, so a row it acts on is the shape it expects. That is the first
133
+ // line and this is the second, because "the door never throws" is a promise
134
+ // to the harbor, which has nobody to hand a rejection to and would count it
135
+ // as no answer at all. Anything unforeseen is the silence a stranger hears.
127
136
  return async function door(bytes) {
128
- const out = await judge(bytes);
137
+ const out = await judge(bytes).catch(() => null);
129
138
  const reply = out?.reply ?? SILENCE;
130
139
  const heard = out?.heard ?? false;
131
140
  try {
@@ -1,4 +1,4 @@
1
- import type { Stance, BeingLike } from '../being/types.ts';
1
+ import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
2
2
  export type Ground = {
3
3
  seed: string | Uint8Array;
4
4
  memory: Record<string, unknown>;
@@ -14,3 +14,6 @@ export type WardPointers = {
14
14
  }>;
15
15
  ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
16
16
  };
17
+ export declare const maker: (objects: WeakMap<object, BeingLike>, ...registries: (Record<string, BeingClass> | undefined)[]) => Ground['instantiate'];
18
+ export declare const entropy: (n: number) => Uint8Array;
19
+ export declare function learnPk(w: WardPointers): Promise<string>;
@@ -1 +1,38 @@
1
- export {};
1
+ // ---- what every harbor builds a ground out of. Three pieces, because every
2
+ // harbor in this tree writes the same three and the spec keeps the harbors
3
+ // themselves apart: what a memory harbor and a real one differ on is the
4
+ // route and the store, and nothing here. A second kit writes its own harbor
5
+ // and may write these again; they are convenience, never contract.
6
+ // The code half of a ground, and the map back to what it made. A class is
7
+ // found by own key only, since `constructor` is a name Object lends every
8
+ // registry, and the first registry holding the name wins, so a ward's own
9
+ // classes stand in front of the harbor's. The object is remembered by the
10
+ // cells the ward handed in, which is how a side reaches a being it made and
11
+ // how that hand follows her out when she is unbooted.
12
+ export const maker = (objects, ...registries) => (className, stance) => {
13
+ const r = registries.find((reg) => reg !== undefined && Object.hasOwn(reg, className));
14
+ const C = r?.[className];
15
+ if (!C)
16
+ return null;
17
+ const obj = new C(stance);
18
+ objects.set(stance.cells, obj);
19
+ return obj;
20
+ };
21
+ // Entropy, from the one place every terrain that runs Quo has it. Every key a
22
+ // ward mints is drawn from this, so a harbor that wants another source hands
23
+ // its own and nothing here has to know.
24
+ export const entropy = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
25
+ // A ward's pk, learned the way anyone learns anything: by asking. The empty
26
+ // ask on the ask pointer is the ward's own describe and its notes carry the
27
+ // pk. A harbor has no other way to it and wants none: the ward mints it from
28
+ // the seed, and a harbor that read it off the seed itself would be a second
29
+ // derivation to keep in step with the first. A ward that answers anything
30
+ // else is not one this harbor can route to, and says so here rather than
31
+ // leaving an undefined pk in a directory.
32
+ export async function learnPk(w) {
33
+ const notes = (await w.ask());
34
+ const pk = notes?.notes?.pk;
35
+ if (typeof pk !== 'string')
36
+ throw new Error('the ward did not say its pk');
37
+ return pk;
38
+ }
@@ -6,9 +6,8 @@ export declare class Heirs {
6
6
  open(heir: string, being: string, id: string): void;
7
7
  close(heir: string): void;
8
8
  gone(heir: string, by: string): boolean;
9
- get(heir: string): Heir | undefined;
10
9
  admits(heir: string, by: string): Heir | null;
11
10
  spend(h: Heir, seq: number): boolean;
12
11
  honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord;
13
- settle(h: Heir, by: string, next: string | null): boolean;
12
+ settle(h: Heir, by: string, next: string | null): void;
14
13
  }
@@ -33,9 +33,6 @@ export class Heirs {
33
33
  const g = this.#p.gone[heir];
34
34
  return !!g && (by === g.current || by === g.announced);
35
35
  }
36
- get(heir) {
37
- return this.#p.heirs[heir];
38
- }
39
36
  // May `by` speak for this heir? Returns the record if so, null if not.
40
37
  // Does not write: the caller verifies the signature first, then settles.
41
38
  admits(heir, by) {
@@ -79,31 +76,33 @@ export class Heirs {
79
76
  // neither: a call that binds nothing must not burn a number on its way to
80
77
  // being refused, or a stranger who cannot be heard would still leave a mark
81
78
  // behind her. Every write below this line is one that is going to hold.
79
+ // This is where a knock is judged and where `unannounced` is said. settle
80
+ // below is the writing alone, and it judges nothing: one owner for the
81
+ // question, one for the answer.
82
82
  honour(h, by, next, seq) {
83
83
  if (h.fresh && (next === null || next === h.current))
84
84
  return 'unannounced'; // a knock without a key of her own binds nothing, and the heir is not a key of her own
85
85
  if (!this.spend(h, seq))
86
86
  return 'repeated';
87
- const settled = this.settle(h, by, next);
88
- this.#wrote(); // the number is spent whether or not the keys settled
89
- return settled ? true : 'unannounced';
87
+ this.settle(h, by, next);
88
+ this.#wrote(); // the number and the keys moved together
89
+ return true;
90
90
  }
91
- // The signature checked out. Settle the keys: a fresh heir rotates at once
92
- // to what it announced and must announce something that is not itself, so
93
- // that the heir dies as it speaks; a current key replaces its
94
- // announcement; an announced key becomes current.
91
+ // The signature checked out and honour has judged. Settle the keys: a fresh
92
+ // heir rotates at once to what it announced, which honour has already held
93
+ // to being something other than itself, so that the heir dies as it speaks;
94
+ // a current key replaces its announcement; an announced key becomes current.
95
95
  settle(h, by, next) {
96
96
  if (h.fresh) {
97
- if (next === null || next === h.current)
98
- return false;
97
+ if (next === null)
98
+ return; // honour judged this. the compiler has not read it, so the narrowing stays.
99
99
  h.current = next;
100
100
  h.announced = null;
101
101
  h.fresh = false;
102
- return true;
102
+ return;
103
103
  }
104
104
  if (by === h.announced)
105
105
  h.current = by;
106
106
  h.announced = next;
107
- return true;
108
107
  }
109
108
  }