@quo-systems/quo 0.2.9 → 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 (78) hide show
  1. package/README.md +19 -10
  2. package/SPEC.md +301 -110
  3. package/dist/being/being.d.ts +2 -2
  4. package/dist/being/being.js +34 -12
  5. package/dist/being/digest.js +26 -8
  6. package/dist/being/index.d.ts +2 -2
  7. package/dist/being/index.js +2 -2
  8. package/dist/being/silence.d.ts +2 -0
  9. package/dist/being/silence.js +12 -0
  10. package/dist/being/types.d.ts +4 -2
  11. package/dist/being/types.js +25 -0
  12. package/dist/conformance/assert.js +40 -6
  13. package/dist/conformance/beings.d.ts +48 -5
  14. package/dist/conformance/beings.js +39 -8
  15. package/dist/conformance/estate.js +110 -21
  16. package/dist/conformance/index.d.ts +5 -2
  17. package/dist/conformance/index.js +165 -7
  18. package/dist/harbor/core.d.ts +5 -2
  19. package/dist/harbor/core.js +195 -45
  20. package/dist/harbor/dial.js +32 -15
  21. package/dist/harbor/index.d.ts +1 -0
  22. package/dist/harbor/index.js +3 -0
  23. package/dist/harbor/memory.d.ts +3 -3
  24. package/dist/harbor/memory.js +7 -12
  25. package/dist/harbor/reach.js +42 -17
  26. package/dist/ward/allowance.js +15 -4
  27. package/dist/ward/arithmetic.d.ts +1 -0
  28. package/dist/ward/arithmetic.js +22 -6
  29. package/dist/ward/cells.d.ts +3 -1
  30. package/dist/ward/cells.js +79 -21
  31. package/dist/ward/door.d.ts +3 -2
  32. package/dist/ward/door.js +38 -10
  33. package/dist/ward/ground.d.ts +5 -1
  34. package/dist/ward/ground.js +38 -1
  35. package/dist/ward/heirs.d.ts +2 -3
  36. package/dist/ward/heirs.js +19 -13
  37. package/dist/ward/index.d.ts +1 -0
  38. package/dist/ward/index.js +3 -0
  39. package/dist/ward/owner.d.ts +6 -27
  40. package/dist/ward/owner.js +59 -33
  41. package/dist/ward/partition.d.ts +3 -0
  42. package/dist/ward/partition.js +109 -4
  43. package/dist/ward/seal.d.ts +1 -0
  44. package/dist/ward/seal.js +41 -13
  45. package/dist/ward/stance.d.ts +7 -3
  46. package/dist/ward/stance.js +156 -66
  47. package/dist/ward/ward.d.ts +10 -0
  48. package/dist/ward/ward.js +123 -51
  49. package/package.json +4 -2
  50. package/src/being/being.ts +33 -11
  51. package/src/being/digest.ts +28 -13
  52. package/src/being/index.ts +2 -2
  53. package/src/being/silence.ts +14 -0
  54. package/src/being/types.ts +39 -5
  55. package/src/conformance/assert.ts +37 -4
  56. package/src/conformance/beings.ts +41 -10
  57. package/src/conformance/estate.ts +107 -20
  58. package/src/conformance/index.ts +188 -13
  59. package/src/harbor/core.ts +203 -46
  60. package/src/harbor/dial.ts +46 -17
  61. package/src/harbor/index.ts +3 -0
  62. package/src/harbor/memory.ts +8 -13
  63. package/src/harbor/reach.ts +47 -21
  64. package/src/ward/allowance.ts +15 -4
  65. package/src/ward/arithmetic.ts +25 -8
  66. package/src/ward/cells.ts +76 -25
  67. package/src/ward/door.ts +38 -12
  68. package/src/ward/ground.ts +52 -3
  69. package/src/ward/heirs.ts +19 -13
  70. package/src/ward/index.ts +3 -0
  71. package/src/ward/owner.ts +65 -46
  72. package/src/ward/partition.ts +109 -5
  73. package/src/ward/seal.ts +41 -12
  74. package/src/ward/stance.ts +163 -64
  75. package/src/ward/ward.ts +124 -52
  76. package/vectors/arithmetic.json +7 -0
  77. package/vectors/framing.json +30 -15
  78. package/vectors/wire.json +4 -4
@@ -5,7 +5,7 @@
5
5
  // is held to the ward's ceiling. The door reads it before anything is done
6
6
  // under it and refuses a budget already gone, as one silence like every other
7
7
  // refusal there. The sender bounds the whole of her ask to the same number,
8
- // and a wait that ran out is silence, never unreached.
8
+ // and a wait that ran out is the word late, never silence and never unreached.
9
9
  //
10
10
  // Each ask is bounded on its own. The time an arriving call has left does not
11
11
  // bound the asks a being makes while answering it: attributing her onward ask
@@ -24,11 +24,22 @@ export const CEILING = { time: 300_000 };
24
24
  // is thirty seconds, silently, because the ceiling is not hers to know.
25
25
  // Asking for nothing at all is the default, which is the whole point.
26
26
  export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
27
- const whole = (n, fallback) => (typeof n === 'number' && Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback);
27
+ // Floored first, then tested: a fraction of a millisecond is not a budget,
28
+ // and falls to the default like every other number that is not a positive
29
+ // whole one.
30
+ const whole = (n, fallback) => {
31
+ const w = typeof n === 'number' ? Math.floor(n) : NaN;
32
+ return Number.isFinite(w) && w > 0 ? w : fallback;
33
+ };
28
34
  return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
29
35
  }
30
- // Whether a budget has anything left to spend. A door reads this on arrival.
31
- 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);
32
43
  // What a wait that ran out comes back as. Its own value, held by nobody
33
44
  // outside the ward, so no answer from any door can be mistaken for it.
34
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<{
@@ -8,7 +8,6 @@
8
8
  // runs on. `test/floor.test.ts` names the floor and probes for it. Subtle is
9
9
  // asynchronous, so everything here is.
10
10
  //
11
- // Ported from an earlier kit's arithmetic. Same bytes, same vectors.
12
11
  // `crypto.subtle` is read at every use and never captured at load. A browser
13
12
  // on a plain http:// origin has `crypto` without `subtle`, and a terrain may
14
13
  // install one after this module is first imported; a reference taken here
@@ -24,7 +23,7 @@ export const SIGNATURE = 64;
24
23
  export const NONCE = 12;
25
24
  export const TAG = 16;
26
25
  const SEAL_INFO = new TextEncoder().encode('quo-seal');
27
- const SEAL_SALT = new Uint8Array(0);
26
+ const SALT = new Uint8Array(0);
28
27
  // A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
29
28
  const ED_SECRET = unhex('302e020100300506032b657004220420');
30
29
  const X_SECRET = unhex('302e020100300506032b656e04220420');
@@ -60,7 +59,12 @@ export function sameBytes(a, b) {
60
59
  diff |= a[at] ^ b[at];
61
60
  return diff === 0;
62
61
  }
63
- // The eight small-order points. A public key among them verifies nothing.
62
+ // The eight small-order points of Ed25519, in their canonical encoding. A
63
+ // public key among them verifies nothing. An encoding whose y coordinate is
64
+ // not reduced, y at or above the field's prime p, names one of the same
65
+ // points under another spelling, and a terrain's verify may accept a zero
66
+ // signature under it; the field has room for the spelling, so it is refused
67
+ // before the list is read.
64
68
  const SMALL_ORDER = [
65
69
  '0100000000000000000000000000000000000000000000000000000000000000',
66
70
  'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
@@ -71,7 +75,11 @@ const SMALL_ORDER = [
71
75
  '26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
72
76
  'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
73
77
  ].map(unhex);
74
- export const smallOrder = (pk) => SMALL_ORDER.some((p) => sameBytes(p, pk));
78
+ // y >= p, read little-endian with the sign bit masked off: the top byte is
79
+ // 0x7f under the mask, every middle byte is 0xff, and the low byte is at
80
+ // least 0xed, which is p's.
81
+ const unreduced = (pk) => (pk[31] & 0x7f) === 0x7f && pk[0] >= 0xed && pk.subarray(1, 31).every((b) => b === 0xff);
82
+ export const smallOrder = (pk) => unreduced(pk) || SMALL_ORDER.some((p) => sameBytes(p, pk));
75
83
  const key32 = (value, what) => {
76
84
  if (!(value instanceof Uint8Array) || value.length !== KEY)
77
85
  throw new Error(`${what} is not a 32-byte key`);
@@ -124,11 +132,19 @@ export async function agree(secret, peerPk) {
124
132
  throw new Error('dead agreement'); // a padlock that was not a real key
125
133
  return shared;
126
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
+ }
127
144
  // One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
128
145
  // no randomness of its own: the key it pairs with is fresh on every message.
129
146
  async function cipherKey(shared, use) {
130
- const material = await subtle().importKey('raw', shared, 'HKDF', false, ['deriveBits']);
131
- 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);
132
148
  return { key: await subtle().importKey('raw', out.subarray(0, KEY), 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
133
149
  }
134
150
  // The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
@@ -1,3 +1,5 @@
1
1
  import type { Cells } from '../being/types.ts';
2
+ export declare const DEPTH = 64;
2
3
  export declare const cellFault: (v: unknown, path: string) => string | null;
3
- export declare const guardCells: (cells: Cells) => Cells;
4
+ export declare const guardCells: (cells: Cells, wrote?: () => void) => Cells;
5
+ export declare const unguarded: (cells: Cells) => Cells;
@@ -1,6 +1,12 @@
1
+ // How deep a value may nest. Every walk over a value in the kit recurses,
2
+ // and a peer chooses the depth of what she answers; a bound keeps a hostile
3
+ // reply from ending a walk in a stack overflow instead of a refusal.
4
+ export const DEPTH = 64;
1
5
  // I-JSON, all the way down. A number that JSON cannot write is not a number
2
- // a harbor can keep, and a key on the prototype is not a key she wrote.
3
- function fault(v, path, seen) {
6
+ // a harbor can keep, a key on the prototype is not a key she wrote, and a
7
+ // hole in a list, or a key named `__proto__`, is a thing JSON writes one way
8
+ // and a runtime reads another.
9
+ function fault(v, path, seen, depth) {
4
10
  if (v === null || typeof v === 'boolean' || typeof v === 'string')
5
11
  return null;
6
12
  if (typeof v === 'number')
@@ -9,11 +15,15 @@ function fault(v, path, seen) {
9
15
  return `${path} is a ${typeof v}, which is not a value`;
10
16
  if (seen.has(v))
11
17
  return `${path} refers back to itself`;
18
+ if (depth >= DEPTH)
19
+ return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
12
20
  seen.add(v);
13
21
  try {
14
22
  if (Array.isArray(v)) {
15
23
  for (let i = 0; i < v.length; i += 1) {
16
- const f = fault(v[i], `${path}[${i}]`, seen);
24
+ if (!(i in v))
25
+ return `${path}[${i}] is a hole, which no harbor can write down`;
26
+ const f = fault(v[i], `${path}[${i}]`, seen, depth + 1);
17
27
  if (f)
18
28
  return f;
19
29
  }
@@ -23,7 +33,9 @@ function fault(v, path, seen) {
23
33
  if (proto !== Object.prototype && proto !== null)
24
34
  return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
25
35
  for (const k of Object.keys(v)) {
26
- const f = fault(v[k], `${path}.${k}`, seen);
36
+ if (k === '__proto__')
37
+ return `${path}.__proto__ is a key no harbor can keep`;
38
+ const f = fault(v[k], `${path}.${k}`, seen, depth + 1);
27
39
  if (f)
28
40
  return f;
29
41
  }
@@ -33,14 +45,44 @@ function fault(v, path, seen) {
33
45
  seen.delete(v);
34
46
  }
35
47
  }
36
- export const cellFault = (v, path) => fault(v, path, new Set());
48
+ export const cellFault = (v, path) => fault(v, path, new Set(), 0);
49
+ // The three keys at the root of her cells that are the ward's: it writes
50
+ // them, she reads them, and a write of hers there is refused like a non-value.
51
+ const WARDS = new Set(['standings', 'occupants', 'class']);
37
52
  // The guard is one proxy at the root and one for every container read through
38
53
  // it, so a write nested three deep is refused the same way a write at the top
39
54
  // is. Wrappers are remembered, so reading the same array twice is the same
40
55
  // object twice and a being may still compare what she holds.
41
56
  const wrapped = new WeakMap();
57
+ const targets = new WeakMap();
42
58
  const guards = new WeakSet();
43
- function guard(target, path) {
59
+ // A key a write may land on: a string that is not `__proto__`, and not one
60
+ // of the ward's at the root. A symbol key is a thing JSON never writes.
61
+ const refuse = (why) => {
62
+ throw new TypeError(`cells hold values: ${why}`);
63
+ };
64
+ function keyFault(t, k, v, path, root) {
65
+ if (typeof k !== 'string')
66
+ refuse(`${path} takes no symbol key`);
67
+ if (k === '__proto__')
68
+ refuse(`${path}.__proto__ is a key no harbor can keep`);
69
+ if (root && WARDS.has(k))
70
+ refuse(`${path}.${k} is the ward's to write`);
71
+ // A list grows by one at its end, or it has holes JSON cannot write. A
72
+ // push sets the slot at its length and then the length: both pass. A
73
+ // length set past what she wrote, or a slot beyond it, would leave holes.
74
+ if (Array.isArray(t)) {
75
+ if (k === 'length') {
76
+ if (typeof v !== 'number' || v > t.length)
77
+ refuse(`${path}.length set past what she wrote would leave holes`);
78
+ return;
79
+ }
80
+ const i = Number(k);
81
+ if (Number.isInteger(i) && i > t.length)
82
+ refuse(`${path}[${i}] would leave a hole`);
83
+ }
84
+ }
85
+ function guard(target, path, wrote, root = false) {
44
86
  if (guards.has(target))
45
87
  return target;
46
88
  const had = wrapped.get(target);
@@ -50,30 +92,46 @@ function guard(target, path) {
50
92
  get(t, k, r) {
51
93
  const v = Reflect.get(t, k, r);
52
94
  // A container reached through her cells is part of her cells.
53
- return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v, `${path}.${String(k)}`) : v;
95
+ return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v, `${path}.${String(k)}`, wrote) : v;
54
96
  },
55
97
  set(t, k, v, r) {
56
- if (typeof k === 'string') {
57
- const f = cellFault(v, `${path}.${k}`);
58
- if (f)
59
- throw new TypeError(`cells hold values: ${f}`);
60
- }
61
- return Reflect.set(t, k, v, r);
98
+ keyFault(t, k, v, path, root);
99
+ const f = cellFault(v, `${path}.${String(k)}`);
100
+ if (f)
101
+ refuse(f);
102
+ const ok = Reflect.set(t, k, v, r);
103
+ wrote();
104
+ return ok;
62
105
  },
63
106
  defineProperty(t, k, d) {
64
- if (typeof k === 'string' && 'value' in d) {
65
- const f = cellFault(d.value, `${path}.${k}`);
66
- if (f)
67
- throw new TypeError(`cells hold values: ${f}`);
68
- }
69
- return Reflect.defineProperty(t, k, d);
107
+ if (!('value' in d))
108
+ refuse(`${path}.${String(k)} is an accessor, which no harbor can keep`);
109
+ keyFault(t, k, d.value, path, root);
110
+ const f = cellFault(d.value, `${path}.${String(k)}`);
111
+ if (f)
112
+ refuse(f);
113
+ const ok = Reflect.defineProperty(t, k, d);
114
+ wrote();
115
+ return ok;
116
+ },
117
+ deleteProperty(t, k) {
118
+ if (root && typeof k === 'string' && WARDS.has(k))
119
+ refuse(`${path}.${k} is the ward's to keep`);
120
+ const ok = Reflect.deleteProperty(t, k);
121
+ wrote();
122
+ return ok;
70
123
  },
71
124
  });
72
125
  wrapped.set(target, p);
126
+ targets.set(p, target);
73
127
  guards.add(p);
74
128
  return p;
75
129
  }
76
130
  // Her cells, guarded. Called once per being at boot; the guarded object is
77
131
  // what goes into the partition and what the stance hands her, so there is no
78
- // second door onto the same cells.
79
- export const guardCells = (cells) => guard(cells, 'cells');
132
+ // second door onto the same cells. Every write that passes says so to the
133
+ // ward, which says so to its harbor.
134
+ export const guardCells = (cells, wrote = () => { }) => guard(cells, 'cells', wrote, true);
135
+ // The cells behind the guard, for the ward alone: the one writer of the
136
+ // three keys that are its own.
137
+ export const unguarded = (cells) => (targets.get(cells) ?? cells);
@@ -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 = {
@@ -13,4 +13,5 @@ export type Judged = {
13
13
  heard: boolean;
14
14
  };
15
15
  export declare function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
16
+ export declare function seen(door: Door, asker: Asker): Promise<string | null>;
16
17
  export declare function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array): (bytes: Uint8Array) => Promise<Judged>;
package/dist/ward/door.js CHANGED
@@ -13,6 +13,7 @@ import { at } from './partition.js';
13
13
  import { openAsk, sealReply, verifyAsk } from './seal.js';
14
14
  import { spent } from './allowance.js';
15
15
  import { KEY, sealingPair } from './arithmetic.js';
16
+ import { cellFault } from './cells.js';
16
17
  const SILENCE = { silence: true };
17
18
  const said = (quo) => ({ quo });
18
19
  // One arrival at one being, already named. Catches every throw. `bound` says
@@ -34,13 +35,22 @@ export async function arrive(door, asker, method, args, bound) {
34
35
  return SILENCE;
35
36
  if (isWord(out))
36
37
  return threw; // a word is the ward's to say, never hers
38
+ // Her answer is held to the rule her args and her cells are held to. A
39
+ // shape JSON would drop or rewrite on the way out, a Date, a Map, a NaN, a
40
+ // cycle, is not hers to make: the far side would read something she never
41
+ // said, or the door itself would fail to write her reply after the number
42
+ // was spent. It is threw, like a word out of her.
43
+ if (cellFault(out, 'answer') !== null)
44
+ return threw;
37
45
  if (method === undefined)
38
46
  return { object: out, seen: null };
39
47
  // The digest rides along, it is not the answer. She has already answered:
40
48
  // a describe that will not run costs the digest, and nothing else.
41
49
  return { object: out, seen: await seen(door, asker) };
42
50
  }
43
- async function seen(door, asker) {
51
+ // Her digest for this asker, or null when her describe threw or fell silent.
52
+ // The owner's describe reads it the same way for every being of the ward.
53
+ export async function seen(door, asker) {
44
54
  try {
45
55
  const bp = await door.being.answer(asker);
46
56
  return isSilence(bp) || isWord(bp) ? null : await digest(bp);
@@ -56,9 +66,10 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
56
66
  return null; // D1. it did not open. there is nobody to answer.
57
67
  const { to, payload, ephemeralPk } = a;
58
68
  const refuse = () => ({ reply: SILENCE, ephemeralPk, heard: false });
59
- // D2, the rest of it. The allowance is read before anything is done under
60
- // it, and hops at zero is refused so that a relay chain invented later
61
- // meets doors that already stop it. Nothing sets hops.
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.
62
73
  if (spent({ time: payload.time }) || payload.hops === 0)
63
74
  return refuse();
64
75
  const args = payload.args ?? {};
@@ -70,7 +81,10 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
70
81
  const pub = pk !== null ? doors.get(pk) : undefined;
71
82
  if (!pub || !(await verifyAsk(a, payload.by)))
72
83
  return refuse();
73
- 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 };
74
88
  }
75
89
  const h = heirs.admits(to, payload.by);
76
90
  if (!h) {
@@ -83,6 +97,13 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
83
97
  }
84
98
  if (!(await verifyAsk(a, payload.by)))
85
99
  return refuse(); // D7, under an admitted key
100
+ // The signature took time, and another arrival on this heir may have been
101
+ // honoured meanwhile: a knock that raced this one and won spent the heir,
102
+ // and the key this ask speaks under may not be admitted any more. The
103
+ // door judges concurrently, so admission is read again now that writing
104
+ // is next, and what changed under the await is judged as it stands.
105
+ if (!heirs.admits(to, payload.by))
106
+ return heirs.gone(to, payload.by) ? { reply: said('removed'), ephemeralPk, heard: true } : refuse();
86
107
  // From here the door has heard a key it holds. Every answer below is
87
108
  // sealed to that key's lid; nothing below is a stranger's.
88
109
  const door = doors.get(h.being);
@@ -102,18 +123,25 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
102
123
  // reply is sealed to the ephemeral pk on its lid, which is all a stranger
103
124
  // holds, and says silence.
104
125
  //
105
- // A lid that is not a key -- a small-order point, of which the two curves
106
- // have several each -- makes a dead agreement, and the seal refuses it. The
107
- // door never throws: that reply is noise, sealed to a key nobody holds.
126
+ // A lid that is not a key, a small-order point, of which the two curves
127
+ // have several each, makes a dead agreement, and the seal refuses it. The
128
+ // door never throws: that reply is noise, a plain silence sealed to a key
129
+ // nobody holds, and the same is written for anything else the seal will
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.
108
136
  return async function door(bytes) {
109
- const out = await judge(bytes);
137
+ const out = await judge(bytes).catch(() => null);
110
138
  const reply = out?.reply ?? SILENCE;
111
139
  const heard = out?.heard ?? false;
112
140
  try {
113
141
  return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
114
142
  }
115
143
  catch {
116
- return { bytes: await sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
144
+ return { bytes: await sealReply(SILENCE, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
117
145
  }
118
146
  };
119
147
  }
@@ -1,8 +1,9 @@
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>;
5
5
  instantiate(className: string, stance: Stance): BeingLike | null;
6
+ wrote?: () => void;
6
7
  carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
7
8
  random(n: number): Uint8Array;
8
9
  };
@@ -13,3 +14,6 @@ export type WardPointers = {
13
14
  }>;
14
15
  ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
15
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
+ }
@@ -2,13 +2,12 @@ import type { DoorWord } from '../being/types.ts';
2
2
  import { type Heir, type Partition } from './partition.ts';
3
3
  export declare class Heirs {
4
4
  #private;
5
- constructor(p: Partition);
5
+ constructor(p: Partition, wrote?: () => void);
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
  }
@@ -4,11 +4,14 @@ import { GONE } from './partition.js';
4
4
  const SPAN = 64;
5
5
  export class Heirs {
6
6
  #p;
7
- constructor(p) {
7
+ #wrote;
8
+ constructor(p, wrote = () => { }) {
8
9
  this.#p = p;
10
+ this.#wrote = wrote;
9
11
  }
10
12
  open(heir, being, id) {
11
13
  this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
14
+ this.#wrote();
12
15
  }
13
16
  // The id was removed. The heir is forgotten, and its last keys are kept
14
17
  // apart, bounded, so that whoever still holds them hears `removed` at the
@@ -22,6 +25,7 @@ export class Heirs {
22
25
  const keys = Object.keys(this.#p.gone);
23
26
  for (const old of keys.slice(0, Math.max(0, keys.length - GONE)))
24
27
  delete this.#p.gone[old];
28
+ this.#wrote();
25
29
  }
26
30
  // May `by` speak for a relation she removed? True only for the keys the
27
31
  // door held when the id went, which nobody but their holder has.
@@ -29,9 +33,6 @@ export class Heirs {
29
33
  const g = this.#p.gone[heir];
30
34
  return !!g && (by === g.current || by === g.announced);
31
35
  }
32
- get(heir) {
33
- return this.#p.heirs[heir];
34
- }
35
36
  // May `by` speak for this heir? Returns the record if so, null if not.
36
37
  // Does not write: the caller verifies the signature first, then settles.
37
38
  admits(heir, by) {
@@ -75,28 +76,33 @@ export class Heirs {
75
76
  // neither: a call that binds nothing must not burn a number on its way to
76
77
  // being refused, or a stranger who cannot be heard would still leave a mark
77
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.
78
82
  honour(h, by, next, seq) {
79
- if (h.fresh && next === null)
80
- return 'unannounced'; // a knock without a key of her own binds nothing
83
+ if (h.fresh && (next === null || next === h.current))
84
+ return 'unannounced'; // a knock without a key of her own binds nothing, and the heir is not a key of her own
81
85
  if (!this.spend(h, seq))
82
86
  return 'repeated';
83
- return this.settle(h, by, next) ? true : 'unannounced';
87
+ this.settle(h, by, next);
88
+ this.#wrote(); // the number and the keys moved together
89
+ return true;
84
90
  }
85
- // The signature checked out. Settle the keys: a fresh heir rotates at once
86
- // to what it announced and must announce something; a current key replaces
87
- // its 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.
88
95
  settle(h, by, next) {
89
96
  if (h.fresh) {
90
97
  if (next === null)
91
- return false; // a knock without a key of her own binds nothing
98
+ return; // honour judged this. the compiler has not read it, so the narrowing stays.
92
99
  h.current = next;
93
100
  h.announced = null;
94
101
  h.fresh = false;
95
- return true;
102
+ return;
96
103
  }
97
104
  if (by === h.announced)
98
105
  h.current = by;
99
106
  h.announced = next;
100
- return true;
101
107
  }
102
108
  }
@@ -1,6 +1,7 @@
1
1
  export { Ward } from './ward.ts';
2
2
  export type { Ground, WardPointers } from './ground.ts';
3
3
  export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
4
+ export { GONE, MINTED } from './partition.ts';
4
5
  export type { AskPayload, ReplyPayload } from './seal.ts';
5
6
  export * as seal from './seal.ts';
6
7
  export * as arithmetic from './arithmetic.ts';
@@ -1,6 +1,9 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // @quo-systems/quo/ward — one function. Every ward is the same ward.
3
3
  export { Ward } from './ward.js';
4
+ // The two bounds on what a partition may hold, for a kit that must keep the
5
+ // same ones and for a test that pins them.
6
+ export { GONE, MINTED } from './partition.js';
4
7
  // The seal and the arithmetic, for a kit in another language to check its
5
8
  // bytes against, and for tests that speak to a door directly.
6
9
  export * as seal from './seal.js';
@@ -1,34 +1,13 @@
1
- import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
1
+ import type { Ask, Asker, Json, JsonObject } from '../being/types.ts';
2
+ import type { Resident } from './ward.ts';
2
3
  export type OwnerSide = {
3
4
  pk: string;
4
- doors: Map<string, {
5
- key: string;
6
- being: {
7
- answer: (...a: never[]) => unknown;
8
- };
9
- cells: {
10
- class?: string;
11
- occupants: Record<string, unknown>;
12
- standings: Record<string, unknown>;
13
- };
14
- stance: {
15
- occupants: {
16
- invite(id: string): Promise<Invitation | null>;
17
- remove(id: string): void;
18
- };
19
- standings: {
20
- knock(inv: Invitation, m?: string, a?: JsonObject, w?: Wanted): Promise<unknown>;
21
- take(id: string, inv: Invitation): Promise<string | null>;
22
- remove(id: string): void;
23
- };
24
- };
25
- }>;
5
+ doors: Map<string, Resident>;
6
+ absent(): Record<string, string | null>;
26
7
  publicKey(): string | null;
27
- instantiate(key: string, className: string): {
28
- key: string;
29
- } | null;
8
+ instantiate(className: string, key: string): Resident | 'threw' | null;
30
9
  unboot(key: string): string[] | null;
31
- setPublic(key: string): void;
10
+ setPublic(key: string | null): void;
32
11
  };
33
12
  export declare const OWNER_ASKS: Ask[];
34
13
  export declare function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json>;