@quo-systems/quo 0.2.9 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +19 -10
  2. package/SPEC.md +161 -89
  3. package/dist/being/being.d.ts +2 -2
  4. package/dist/being/being.js +27 -11
  5. package/dist/being/digest.js +11 -7
  6. package/dist/being/types.d.ts +2 -2
  7. package/dist/conformance/estate.js +1 -3
  8. package/dist/conformance/index.js +1 -1
  9. package/dist/harbor/core.d.ts +2 -0
  10. package/dist/harbor/core.js +114 -25
  11. package/dist/harbor/dial.js +5 -3
  12. package/dist/harbor/memory.d.ts +1 -1
  13. package/dist/harbor/memory.js +3 -2
  14. package/dist/harbor/reach.js +32 -7
  15. package/dist/ward/allowance.js +8 -2
  16. package/dist/ward/arithmetic.js +11 -3
  17. package/dist/ward/cells.d.ts +3 -1
  18. package/dist/ward/cells.js +79 -21
  19. package/dist/ward/door.d.ts +1 -0
  20. package/dist/ward/door.js +24 -5
  21. package/dist/ward/ground.d.ts +1 -0
  22. package/dist/ward/heirs.d.ts +1 -1
  23. package/dist/ward/heirs.js +15 -8
  24. package/dist/ward/owner.d.ts +5 -26
  25. package/dist/ward/owner.js +24 -15
  26. package/dist/ward/seal.js +3 -0
  27. package/dist/ward/stance.d.ts +4 -1
  28. package/dist/ward/stance.js +116 -54
  29. package/dist/ward/ward.d.ts +10 -0
  30. package/dist/ward/ward.js +87 -36
  31. package/package.json +4 -2
  32. package/src/being/being.ts +26 -10
  33. package/src/being/digest.ts +13 -11
  34. package/src/being/types.ts +10 -4
  35. package/src/conformance/estate.ts +1 -3
  36. package/src/conformance/index.ts +1 -1
  37. package/src/harbor/core.ts +110 -26
  38. package/src/harbor/dial.ts +6 -4
  39. package/src/harbor/memory.ts +3 -2
  40. package/src/harbor/reach.ts +38 -11
  41. package/src/ward/allowance.ts +8 -2
  42. package/src/ward/arithmetic.ts +11 -3
  43. package/src/ward/cells.ts +76 -25
  44. package/src/ward/door.ts +22 -5
  45. package/src/ward/ground.ts +9 -2
  46. package/src/ward/heirs.ts +13 -6
  47. package/src/ward/owner.ts +24 -24
  48. package/src/ward/seal.ts +2 -0
  49. package/src/ward/stance.ts +117 -48
  50. package/src/ward/ward.ts +86 -35
  51. package/vectors/arithmetic.json +7 -0
  52. package/vectors/framing.json +14 -5
package/src/ward/cells.ts CHANGED
@@ -3,12 +3,12 @@
3
3
  // persist a being's cells as it likes: as this process's objects, as a row, as
4
4
  // a line of JSON on a disk. That promise is only worth what the cells honour.
5
5
  //
6
- // Nothing checked it. A being could write a Map, a Date, an undefined, a NaN.
7
- // In a memory harbor the object survives, because there is nowhere for it to
8
- // go and come back from. Through any harbor that keeps values it silently
9
- // becomes {}, or a string, or absent, or null and she is never told which
10
- // harbor she is standing in. That is the memory harbor lying to her, which is
11
- // the one thing it must never do.
6
+ // Unchecked, a being could write a Map, a Date, an undefined, a NaN. In a
7
+ // memory harbor the object would survive, because there is nowhere for it to
8
+ // go and come back from. Through any harbor that keeps values it would
9
+ // silently become {}, or a string, or absent, or null, and she is never told
10
+ // which harbor she is standing in. That would be the memory harbor lying to
11
+ // her, which is the one thing it must never do.
12
12
  //
13
13
  // So the cells refuse the write. Loud, where she wrote it, in her own frame:
14
14
  // a being holds three answers and a throw is not one of them, so this throw
@@ -17,18 +17,27 @@
17
17
  // that a harbor would have to lie about later.
18
18
  import type { Cells, Json } from '../being/types.ts';
19
19
 
20
+ // How deep a value may nest. Every walk over a value in the kit recurses,
21
+ // and a peer chooses the depth of what she answers; a bound keeps a hostile
22
+ // reply from ending a walk in a stack overflow instead of a refusal.
23
+ export const DEPTH = 64;
24
+
20
25
  // I-JSON, all the way down. A number that JSON cannot write is not a number
21
- // a harbor can keep, and a key on the prototype is not a key she wrote.
22
- function fault(v: unknown, path: string, seen: Set<object>): string | null {
26
+ // a harbor can keep, a key on the prototype is not a key she wrote, and a
27
+ // hole in a list, or a key named `__proto__`, is a thing JSON writes one way
28
+ // and a runtime reads another.
29
+ function fault(v: unknown, path: string, seen: Set<object>, depth: number): string | null {
23
30
  if (v === null || typeof v === 'boolean' || typeof v === 'string') return null;
24
31
  if (typeof v === 'number') return Number.isFinite(v) ? null : `${path} is ${String(v)}, which no harbor can write down`;
25
32
  if (typeof v !== 'object') return `${path} is a ${typeof v}, which is not a value`;
26
33
  if (seen.has(v)) return `${path} refers back to itself`;
34
+ if (depth >= DEPTH) return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
27
35
  seen.add(v);
28
36
  try {
29
37
  if (Array.isArray(v)) {
30
38
  for (let i = 0; i < v.length; i += 1) {
31
- const f = fault(v[i], `${path}[${i}]`, seen);
39
+ if (!(i in v)) return `${path}[${i}] is a hole, which no harbor can write down`;
40
+ const f = fault(v[i], `${path}[${i}]`, seen, depth + 1);
32
41
  if (f) return f;
33
42
  }
34
43
  return null;
@@ -36,7 +45,8 @@ function fault(v: unknown, path: string, seen: Set<object>): string | null {
36
45
  const proto = Object.getPrototypeOf(v);
37
46
  if (proto !== Object.prototype && proto !== null) return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
38
47
  for (const k of Object.keys(v)) {
39
- const f = fault((v as Record<string, unknown>)[k], `${path}.${k}`, seen);
48
+ if (k === '__proto__') return `${path}.__proto__ is a key no harbor can keep`;
49
+ const f = fault((v as Record<string, unknown>)[k], `${path}.${k}`, seen, depth + 1);
40
50
  if (f) return f;
41
51
  }
42
52
  return null;
@@ -45,16 +55,43 @@ function fault(v: unknown, path: string, seen: Set<object>): string | null {
45
55
  }
46
56
  }
47
57
 
48
- export const cellFault = (v: unknown, path: string): string | null => fault(v, path, new Set());
58
+ export const cellFault = (v: unknown, path: string): string | null => fault(v, path, new Set(), 0);
59
+
60
+ // The three keys at the root of her cells that are the ward's: it writes
61
+ // them, she reads them, and a write of hers there is refused like a non-value.
62
+ const WARDS = new Set(['standings', 'occupants', 'class']);
49
63
 
50
64
  // The guard is one proxy at the root and one for every container read through
51
65
  // it, so a write nested three deep is refused the same way a write at the top
52
66
  // is. Wrappers are remembered, so reading the same array twice is the same
53
67
  // object twice and a being may still compare what she holds.
54
68
  const wrapped = new WeakMap<object, object>();
69
+ const targets = new WeakMap<object, object>();
55
70
  const guards = new WeakSet();
56
71
 
57
- function guard<T extends object>(target: T, path: string): T {
72
+ // A key a write may land on: a string that is not `__proto__`, and not one
73
+ // of the ward's at the root. A symbol key is a thing JSON never writes.
74
+ const refuse: (why: string) => never = (why) => {
75
+ throw new TypeError(`cells hold values: ${why}`);
76
+ };
77
+ function keyFault(t: object, k: string | symbol, v: unknown, path: string, root: boolean): void {
78
+ if (typeof k !== 'string') refuse(`${path} takes no symbol key`);
79
+ if (k === '__proto__') refuse(`${path}.__proto__ is a key no harbor can keep`);
80
+ if (root && WARDS.has(k)) refuse(`${path}.${k} is the ward's to write`);
81
+ // A list grows by one at its end, or it has holes JSON cannot write. A
82
+ // push sets the slot at its length and then the length: both pass. A
83
+ // length set past what she wrote, or a slot beyond it, would leave holes.
84
+ if (Array.isArray(t)) {
85
+ if (k === 'length') {
86
+ if (typeof v !== 'number' || v > t.length) refuse(`${path}.length set past what she wrote would leave holes`);
87
+ return;
88
+ }
89
+ const i = Number(k);
90
+ if (Number.isInteger(i) && i > t.length) refuse(`${path}[${i}] would leave a hole`);
91
+ }
92
+ }
93
+
94
+ function guard<T extends object>(target: T, path: string, wrote: () => void, root = false): T {
58
95
  if (guards.has(target)) return target;
59
96
  const had = wrapped.get(target);
60
97
  if (had) return had as T;
@@ -62,29 +99,43 @@ function guard<T extends object>(target: T, path: string): T {
62
99
  get(t, k, r) {
63
100
  const v = Reflect.get(t, k, r);
64
101
  // A container reached through her cells is part of her cells.
65
- return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v as object, `${path}.${String(k)}`) : v;
102
+ return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v as object, `${path}.${String(k)}`, wrote) : v;
66
103
  },
67
104
  set(t, k, v, r) {
68
- if (typeof k === 'string') {
69
- const f = cellFault(v, `${path}.${k}`);
70
- if (f) throw new TypeError(`cells hold values: ${f}`);
71
- }
72
- return Reflect.set(t, k, v, r);
105
+ keyFault(t, k, v, path, root);
106
+ const f = cellFault(v, `${path}.${String(k)}`);
107
+ if (f) refuse(f);
108
+ const ok = Reflect.set(t, k, v, r);
109
+ wrote();
110
+ return ok;
73
111
  },
74
112
  defineProperty(t, k, d) {
75
- if (typeof k === 'string' && 'value' in d) {
76
- const f = cellFault(d.value, `${path}.${k}`);
77
- if (f) throw new TypeError(`cells hold values: ${f}`);
78
- }
79
- return Reflect.defineProperty(t, k, d);
113
+ if (!('value' in d)) refuse(`${path}.${String(k)} is an accessor, which no harbor can keep`);
114
+ keyFault(t, k, d.value, path, root);
115
+ const f = cellFault(d.value, `${path}.${String(k)}`);
116
+ if (f) refuse(f);
117
+ const ok = Reflect.defineProperty(t, k, d);
118
+ wrote();
119
+ return ok;
120
+ },
121
+ deleteProperty(t, k) {
122
+ if (root && typeof k === 'string' && WARDS.has(k)) refuse(`${path}.${k} is the ward's to keep`);
123
+ const ok = Reflect.deleteProperty(t, k);
124
+ wrote();
125
+ return ok;
80
126
  },
81
127
  });
82
128
  wrapped.set(target, p);
129
+ targets.set(p, target);
83
130
  guards.add(p);
84
131
  return p;
85
132
  }
86
133
 
87
134
  // Her cells, guarded. Called once per being at boot; the guarded object is
88
135
  // what goes into the partition and what the stance hands her, so there is no
89
- // second door onto the same cells.
90
- export const guardCells = (cells: Cells): Cells => guard(cells as unknown as Record<string, Json>, 'cells') as unknown as Cells;
136
+ // second door onto the same cells. Every write that passes says so to the
137
+ // ward, which says so to its harbor.
138
+ export const guardCells = (cells: Cells, wrote: () => void = () => {}): Cells => guard(cells as unknown as Record<string, Json>, 'cells', wrote, true) as unknown as Cells;
139
+ // The cells behind the guard, for the ward alone: the one writer of the
140
+ // three keys that are its own.
141
+ export const unguarded = (cells: Cells): Cells => (targets.get(cells as unknown as object) ?? cells) as Cells;
package/src/ward/door.ts CHANGED
@@ -15,6 +15,7 @@ import type { Heirs } from './heirs.ts';
15
15
  import { openAsk, sealReply, verifyAsk, type ReplyPayload, type WardKey } from './seal.ts';
16
16
  import { spent } from './allowance.ts';
17
17
  import { KEY, sealingPair } from './arithmetic.ts';
18
+ import { cellFault } from './cells.ts';
18
19
 
19
20
  export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, unknown> } };
20
21
  export type Judged = { bytes: Uint8Array; heard: boolean };
@@ -37,13 +38,21 @@ export async function arrive(door: Door, asker: Asker, method: string | undefine
37
38
  // on the way out and the far side reads back as a fourth word.
38
39
  if (out === undefined || isSilence(out)) return SILENCE;
39
40
  if (isWord(out)) return threw; // a word is the ward's to say, never hers
41
+ // Her answer is held to the rule her args and her cells are held to. A
42
+ // shape JSON would drop or rewrite on the way out, a Date, a Map, a NaN, a
43
+ // cycle, is not hers to make: the far side would read something she never
44
+ // said, or the door itself would fail to write her reply after the number
45
+ // was spent. It is threw, like a word out of her.
46
+ if (cellFault(out, 'answer') !== null) return threw;
40
47
  if (method === undefined) return { object: out, seen: null };
41
48
  // The digest rides along, it is not the answer. She has already answered:
42
49
  // a describe that will not run costs the digest, and nothing else.
43
50
  return { object: out, seen: await seen(door, asker) };
44
51
  }
45
52
 
46
- async function seen(door: Door, asker: Asker): Promise<string | null> {
53
+ // Her digest for this asker, or null when her describe threw or fell silent.
54
+ // The owner's describe reads it the same way for every being of the ward.
55
+ export async function seen(door: Door, asker: Asker): Promise<string | null> {
47
56
  try {
48
57
  const bp = await door.being.answer(asker);
49
58
  return isSilence(bp) || isWord(bp) ? null : await digest(bp);
@@ -83,6 +92,12 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
83
92
  return { reply: said('removed'), ephemeralPk, heard: true };
84
93
  }
85
94
  if (!(await verifyAsk(a, payload.by))) return refuse(); // D7, under an admitted key
95
+ // The signature took time, and another arrival on this heir may have been
96
+ // honoured meanwhile: a knock that raced this one and won spent the heir,
97
+ // and the key this ask speaks under may not be admitted any more. The
98
+ // door judges concurrently, so admission is read again now that writing
99
+ // is next, and what changed under the await is judged as it stands.
100
+ if (!heirs.admits(to, payload.by)) return heirs.gone(to, payload.by) ? { reply: said('removed'), ephemeralPk, heard: true } : refuse();
86
101
  // From here the door has heard a key it holds. Every answer below is
87
102
  // sealed to that key's lid; nothing below is a stranger's.
88
103
  const door = doors.get(h.being);
@@ -100,9 +115,11 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
100
115
  // reply is sealed to the ephemeral pk on its lid, which is all a stranger
101
116
  // holds, and says silence.
102
117
  //
103
- // A lid that is not a key -- a small-order point, of which the two curves
104
- // have several each -- makes a dead agreement, and the seal refuses it. The
105
- // door never throws: that reply is noise, sealed to a key nobody holds.
118
+ // A lid that is not a key, a small-order point, of which the two curves
119
+ // have several each, makes a dead agreement, and the seal refuses it. The
120
+ // door never throws: that reply is noise, a plain silence sealed to a key
121
+ // nobody holds, and the same is written for anything else the seal will
122
+ // not take.
106
123
  return async function door(bytes: Uint8Array): Promise<Judged> {
107
124
  const out = await judge(bytes);
108
125
  const reply = out?.reply ?? SILENCE;
@@ -110,7 +127,7 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
110
127
  try {
111
128
  return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
112
129
  } catch {
113
- return { bytes: await sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
130
+ return { bytes: await sealReply(SILENCE, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
114
131
  }
115
132
  };
116
133
  }
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The ground. The one object a harbor passes a ward at birth. Five things,
3
- // never a sixth. Everything a runtime differs on arrives here, which is
2
+ // The ground. The one object a harbor passes a ward at birth. Six things,
3
+ // never a seventh. Everything a runtime differs on arrives here, which is
4
4
  // why the ward itself knows no runtime.
5
5
  import type { Stance, BeingLike } from '../being/types.ts';
6
6
 
@@ -8,6 +8,13 @@ export type Ground = {
8
8
  seed: string | Uint8Array; // the ward derives its pk from it and nothing else
9
9
  memory: Record<string, unknown>; // the partition. the ward's files. opaque to the harbor
10
10
  instantiate(className: string, stance: Stance): BeingLike | null; // the code half
11
+ // The ward wrote its partition. It says so after every write, a key
12
+ // rotated, a relation taken, a cell she set, and says nothing else: it
13
+ // never learns whether anything was kept. A harbor that keeps the
14
+ // partition saves after this, when it likes and in the order it was told,
15
+ // so a being driven in process is kept the way one reached through a door
16
+ // is. A harbor that keeps nothing leaves it out.
17
+ wrote?: () => void;
11
18
  // Sealed bytes to a ward pk. What comes back, or undefined.
12
19
  //
13
20
  // undefined is a promise, not a shrug: no door was reached, and nothing was
package/src/ward/heirs.ts CHANGED
@@ -13,11 +13,14 @@ const SPAN = 64;
13
13
 
14
14
  export class Heirs {
15
15
  #p: Partition;
16
- constructor(p: Partition) {
16
+ #wrote: () => void;
17
+ constructor(p: Partition, wrote: () => void = () => {}) {
17
18
  this.#p = p;
19
+ this.#wrote = wrote;
18
20
  }
19
21
  open(heir: string, being: string, id: string): void {
20
22
  this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
23
+ this.#wrote();
21
24
  }
22
25
  // The id was removed. The heir is forgotten, and its last keys are kept
23
26
  // apart, bounded, so that whoever still holds them hears `removed` at the
@@ -29,6 +32,7 @@ export class Heirs {
29
32
  this.#p.gone[heir] = { current: h.current, announced: h.announced };
30
33
  const keys = Object.keys(this.#p.gone);
31
34
  for (const old of keys.slice(0, Math.max(0, keys.length - GONE))) delete this.#p.gone[old];
35
+ this.#wrote();
32
36
  }
33
37
  // May `by` speak for a relation she removed? True only for the keys the
34
38
  // door held when the id went, which nobody but their holder has.
@@ -79,17 +83,20 @@ export class Heirs {
79
83
  // being refused, or a stranger who cannot be heard would still leave a mark
80
84
  // behind her. Every write below this line is one that is going to hold.
81
85
  honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord {
82
- if (h.fresh && next === null) return 'unannounced'; // a knock without a key of her own binds nothing
86
+ if (h.fresh && (next === null || next === h.current)) return 'unannounced'; // a knock without a key of her own binds nothing, and the heir is not a key of her own
83
87
  if (!this.spend(h, seq)) return 'repeated';
84
- return this.settle(h, by, next) ? true : 'unannounced';
88
+ const settled = this.settle(h, by, next);
89
+ this.#wrote(); // the number is spent whether or not the keys settled
90
+ return settled ? true : 'unannounced';
85
91
  }
86
92
 
87
93
  // The signature checked out. Settle the keys: a fresh heir rotates at once
88
- // to what it announced and must announce something; a current key replaces
89
- // its announcement; an announced key becomes current.
94
+ // to what it announced and must announce something that is not itself, so
95
+ // that the heir dies as it speaks; a current key replaces its
96
+ // announcement; an announced key becomes current.
90
97
  settle(h: Heir, by: string, next: string | null): boolean {
91
98
  if (h.fresh) {
92
- if (next === null) return false; // a knock without a key of her own binds nothing
99
+ if (next === null || next === h.current) return false;
93
100
  h.current = next;
94
101
  h.announced = null;
95
102
  h.fresh = false;
package/src/ward/owner.ts CHANGED
@@ -9,23 +9,18 @@
9
9
  // her cells under the id the owner gave; remove takes a relation out of her
10
10
  // by id, the mirror of it.
11
11
  import { isSilence, isWord, wordOf } from '../being/silence.ts';
12
- import { digest } from '../being/digest.ts';
13
12
  import { OWNER } from '../being/types.ts';
14
13
  import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
14
+ import { put } from './partition.ts';
15
+ import { seen } from './door.ts';
16
+ import type { Booted } from './ward.ts';
15
17
 
16
18
  export type OwnerSide = {
17
19
  pk: string;
18
- doors: Map<
19
- string,
20
- {
21
- key: string;
22
- being: { answer: (...a: never[]) => unknown };
23
- cells: { class?: string; occupants: Record<string, unknown>; standings: Record<string, unknown> };
24
- stance: { occupants: { invite(id: string): Promise<Invitation | null>; remove(id: string): void }; standings: { knock(inv: Invitation, m?: string, a?: JsonObject, w?: Wanted): Promise<unknown>; take(id: string, inv: Invitation): Promise<string | null>; remove(id: string): void } };
25
- }
26
- >;
20
+ doors: Map<string, Booted>;
21
+ absent(): Record<string, string | null>; // the rows no door holds this run: key to class
27
22
  publicKey(): string | null;
28
- instantiate(key: string, className: string): { key: string } | null;
23
+ instantiate(key: string, className: string): Booted | 'threw' | null;
29
24
  unboot(key: string): string[] | null;
30
25
  setPublic(key: string): void;
31
26
  };
@@ -34,7 +29,7 @@ const str = { type: 'string' };
34
29
  export const OWNER_ASKS: Ask[] = [
35
30
  { name: 'boot', description: 'boot a being by class name, under a key the owner chooses', input: { type: 'object', properties: { key: str, class: str }, required: ['key', 'class'] } },
36
31
  { name: 'public', description: 'mark a booted being as the one public being of the ward, reached by anyone at the bare pk', input: { type: 'object', properties: { key: str }, required: ['key'] } },
37
- { name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
32
+ { name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by, with the notes she will read on it; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str, notes: { type: 'object' } }, required: ['being', 'id'] } },
38
33
  {
39
34
  name: 'knock',
40
35
  description: 'knock for a being of the ward with an invitation, and take the standing under id if answered; being is a key booted, or { boot: class, key } to boot her first',
@@ -56,15 +51,18 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
56
51
  className = word(args.class);
57
52
  if (key === null || className === null) return { error: 'no such class, or key taken' };
58
53
  const door = w.instantiate(key, className);
54
+ if (door === 'threw') return { error: 'threw at birth' };
59
55
  return door ? { booted: door.key } : { error: 'no such class, or key taken' };
60
56
  }
61
57
  if (method === 'public') {
62
58
  // A ward may have one public being, and no more. Marking a second would
63
59
  // leave the first holding every relation she had, reachable by nobody at
64
- // the bare pk, and told by nobody that she had been replaced.
60
+ // the bare pk, and told by nobody that she had been replaced. One absent
61
+ // this run is reachable by nobody already, so the mark may move off her.
65
62
  const key = word(args.key);
66
63
  if (key === null || key === w.pk || !w.doors.has(key)) return { error: 'no such being' };
67
- if (w.publicKey() !== null && w.publicKey() !== key) return { error: 'a ward has one public being' };
64
+ const standing = w.publicKey();
65
+ if (standing !== null && standing !== key && w.doors.has(standing)) return { error: 'a ward has one public being' };
68
66
  w.setPublic(key);
69
67
  return { public: key };
70
68
  }
@@ -77,8 +75,13 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
77
75
  id = word(args.id);
78
76
  if (being === null || id === null) return { error: 'no such being' };
79
77
  if (being === w.pk && asker.id !== OWNER) return { error: 'no such being' };
78
+ // The notes are the terms the owner mints under, and the being reads them
79
+ // on her gate. Placing an occupant is the owner's, and saying what it may
80
+ // do is part of placing it; the work behind the gate is still hers alone.
81
+ const notes = args.notes;
80
82
  const door = w.doors.get(being);
81
- return door ? await door.stance.occupants.invite(id) : { error: 'no such being' };
83
+ if (!door) return { error: 'no such being' };
84
+ return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes as JsonObject) : undefined);
82
85
  }
83
86
  if (method === 'knock') {
84
87
  const b = args.being as string | { boot: unknown; key: unknown };
@@ -90,6 +93,7 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
90
93
  className = word(b.boot);
91
94
  if (key === null || className === null) return { error: 'no such being' };
92
95
  const made = w.instantiate(key, className);
96
+ if (made === 'threw') return { error: 'threw at birth' };
93
97
  door = made ? w.doors.get(made.key) : w.doors.get(key);
94
98
  }
95
99
  const id = word(args.id);
@@ -139,20 +143,16 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
139
143
  }
140
144
 
141
145
  // The ward's describe for its owner: its beings, their classes, a digest each,
142
- // as she would describe herself to the owner.
146
+ // as she would describe herself to the owner. A being absent this run is
147
+ // listed too, with a null digest and absent true, so the owner can see her
148
+ // and take her out.
143
149
  async function describe(w: OwnerSide): Promise<Json> {
144
150
  const beings: Record<string, Json> = {};
145
151
  for (const [key, door] of w.doors) {
146
152
  if (key === w.pk) continue;
147
- let d: string | null = null;
148
- try {
149
- const bp = await (door.being.answer as (a: unknown) => unknown)({ id: OWNER });
150
- d = isSilence(bp) || isWord(bp) ? null : await digest(bp as Json);
151
- } catch {
152
- // she threw where she was asked. there is no blueprint, and d stays null.
153
- }
154
- beings[key] = { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d };
153
+ put(beings, key, { class: door.cells.class ?? null, public: w.publicKey() === key, digest: await seen(door, { id: OWNER }) });
155
154
  }
155
+ for (const [key, cls] of Object.entries(w.absent())) put(beings, key, { class: cls, public: w.publicKey() === key, digest: null, absent: true });
156
156
  return {
157
157
  asks: OWNER_ASKS,
158
158
  notes: { pk: w.pk, beings },
package/src/ward/seal.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  import type { DoorWord, Json, JsonObject } from '../being/types.ts';
23
23
  import { isDoorWord } from '../being/silence.ts';
24
24
  import { KEY, SIGNATURE, box, concat, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.ts';
25
+ import { cellFault } from './cells.ts';
25
26
 
26
27
  const utf8 = new TextEncoder();
27
28
  const text = new TextDecoder();
@@ -87,6 +88,7 @@ export async function openAsk(bytes: Uint8Array, padlockSecret: Uint8Array): Pro
87
88
  if (payload.next !== null && !(typeof payload.next === 'string' && /^[0-9a-f]{64}$/.test(payload.next))) return null;
88
89
  if (payload.method !== undefined && typeof payload.method !== 'string') return null; // a name, or the empty ask. never a number, never an object.
89
90
  if (payload.args !== undefined && (payload.args === null || typeof payload.args !== 'object' || Array.isArray(payload.args))) return null; // args are one object, or absent. a string or a list is not an ask.
91
+ if (payload.args !== undefined && cellFault(payload.args, 'args') !== null) return null; // and values all the way down: no key named __proto__, no nesting past the bound
90
92
  if (!Number.isSafeInteger(payload.seq) || payload.seq < 1) return null; // her count for this relation. one and up, and a whole number.
91
93
  // The allowance. A whole number, and one that has already run out is not an
92
94
  // ask this door will open: it is refused as the bytes it is, above.