@quo-systems/quo 0.2.8 → 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
@@ -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);
@@ -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);
@@ -83,6 +93,13 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
83
93
  }
84
94
  if (!(await verifyAsk(a, payload.by)))
85
95
  return refuse(); // D7, under an admitted key
96
+ // The signature took time, and another arrival on this heir may have been
97
+ // honoured meanwhile: a knock that raced this one and won spent the heir,
98
+ // and the key this ask speaks under may not be admitted any more. The
99
+ // door judges concurrently, so admission is read again now that writing
100
+ // is next, and what changed under the await is judged as it stands.
101
+ if (!heirs.admits(to, payload.by))
102
+ return heirs.gone(to, payload.by) ? { reply: said('removed'), ephemeralPk, heard: true } : refuse();
86
103
  // From here the door has heard a key it holds. Every answer below is
87
104
  // sealed to that key's lid; nothing below is a stranger's.
88
105
  const door = doors.get(h.being);
@@ -102,9 +119,11 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
102
119
  // reply is sealed to the ephemeral pk on its lid, which is all a stranger
103
120
  // holds, and says silence.
104
121
  //
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.
122
+ // A lid that is not a key, a small-order point, of which the two curves
123
+ // have several each, makes a dead agreement, and the seal refuses it. The
124
+ // door never throws: that reply is noise, a plain silence sealed to a key
125
+ // nobody holds, and the same is written for anything else the seal will
126
+ // not take.
108
127
  return async function door(bytes) {
109
128
  const out = await judge(bytes);
110
129
  const reply = out?.reply ?? SILENCE;
@@ -113,7 +132,7 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
113
132
  return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
114
133
  }
115
134
  catch {
116
- return { bytes: await sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
135
+ return { bytes: await sealReply(SILENCE, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
117
136
  }
118
137
  };
119
138
  }
@@ -3,6 +3,7 @@ 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
  };
@@ -2,7 +2,7 @@ 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;
@@ -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.
@@ -76,19 +80,22 @@ export class Heirs {
76
80
  // being refused, or a stranger who cannot be heard would still leave a mark
77
81
  // behind her. Every write below this line is one that is going to hold.
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
+ 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';
84
90
  }
85
91
  // 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.
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.
88
95
  settle(h, by, next) {
89
96
  if (h.fresh) {
90
- if (next === null)
91
- return false; // a knock without a key of her own binds nothing
97
+ if (next === null || next === h.current)
98
+ return false;
92
99
  h.current = next;
93
100
  h.announced = null;
94
101
  h.fresh = false;
@@ -1,32 +1,11 @@
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 { Booted } 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, Booted>;
6
+ absent(): Record<string, string | null>;
26
7
  publicKey(): string | null;
27
- instantiate(key: string, className: string): {
28
- key: string;
29
- } | null;
8
+ instantiate(key: string, className: string): Booted | 'threw' | null;
30
9
  unboot(key: string): string[] | null;
31
10
  setPublic(key: string): void;
32
11
  };
@@ -9,13 +9,14 @@
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.js';
12
- import { digest } from '../being/digest.js';
13
12
  import { OWNER } from '../being/types.js';
13
+ import { put } from './partition.js';
14
+ import { seen } from './door.js';
14
15
  const str = { type: 'string' };
15
16
  export const OWNER_ASKS = [
16
17
  { 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'] } },
17
18
  { 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'] } },
18
- { 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'] } },
19
+ { 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'] } },
19
20
  {
20
21
  name: 'knock',
21
22
  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',
@@ -36,16 +37,20 @@ export async function ownerAnswer(w, asker, method, args) {
36
37
  if (key === null || className === null)
37
38
  return { error: 'no such class, or key taken' };
38
39
  const door = w.instantiate(key, className);
40
+ if (door === 'threw')
41
+ return { error: 'threw at birth' };
39
42
  return door ? { booted: door.key } : { error: 'no such class, or key taken' };
40
43
  }
41
44
  if (method === 'public') {
42
45
  // A ward may have one public being, and no more. Marking a second would
43
46
  // leave the first holding every relation she had, reachable by nobody at
44
- // the bare pk, and told by nobody that she had been replaced.
47
+ // the bare pk, and told by nobody that she had been replaced. One absent
48
+ // this run is reachable by nobody already, so the mark may move off her.
45
49
  const key = word(args.key);
46
50
  if (key === null || key === w.pk || !w.doors.has(key))
47
51
  return { error: 'no such being' };
48
- if (w.publicKey() !== null && w.publicKey() !== key)
52
+ const standing = w.publicKey();
53
+ if (standing !== null && standing !== key && w.doors.has(standing))
49
54
  return { error: 'a ward has one public being' };
50
55
  w.setPublic(key);
51
56
  return { public: key };
@@ -60,8 +65,14 @@ export async function ownerAnswer(w, asker, method, args) {
60
65
  return { error: 'no such being' };
61
66
  if (being === w.pk && asker.id !== OWNER)
62
67
  return { error: 'no such being' };
68
+ // The notes are the terms the owner mints under, and the being reads them
69
+ // on her gate. Placing an occupant is the owner's, and saying what it may
70
+ // do is part of placing it; the work behind the gate is still hers alone.
71
+ const notes = args.notes;
63
72
  const door = w.doors.get(being);
64
- return door ? await door.stance.occupants.invite(id) : { error: 'no such being' };
73
+ if (!door)
74
+ return { error: 'no such being' };
75
+ return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? notes : undefined);
65
76
  }
66
77
  if (method === 'knock') {
67
78
  const b = args.being;
@@ -73,6 +84,8 @@ export async function ownerAnswer(w, asker, method, args) {
73
84
  if (key === null || className === null)
74
85
  return { error: 'no such being' };
75
86
  const made = w.instantiate(key, className);
87
+ if (made === 'threw')
88
+ return { error: 'threw at birth' };
76
89
  door = made ? w.doors.get(made.key) : w.doors.get(key);
77
90
  }
78
91
  const id = word(args.id);
@@ -130,22 +143,18 @@ export async function ownerAnswer(w, asker, method, args) {
130
143
  return { error: 'unknown ask' };
131
144
  }
132
145
  // The ward's describe for its owner: its beings, their classes, a digest each,
133
- // 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.
134
149
  async function describe(w) {
135
150
  const beings = {};
136
151
  for (const [key, door] of w.doors) {
137
152
  if (key === w.pk)
138
153
  continue;
139
- let d = null;
140
- try {
141
- const bp = await door.being.answer({ id: OWNER });
142
- d = isSilence(bp) || isWord(bp) ? null : await digest(bp);
143
- }
144
- catch {
145
- // she threw where she was asked. there is no blueprint, and d stays null.
146
- }
147
- beings[key] = { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d };
154
+ put(beings, key, { class: door.cells.class ?? null, public: w.publicKey() === key, digest: await seen(door, { id: OWNER }) });
148
155
  }
156
+ for (const [key, cls] of Object.entries(w.absent()))
157
+ put(beings, key, { class: cls, public: w.publicKey() === key, digest: null, absent: true });
149
158
  return {
150
159
  asks: OWNER_ASKS,
151
160
  notes: { pk: w.pk, beings },
package/dist/ward/seal.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { isDoorWord } from '../being/silence.js';
2
2
  import { KEY, SIGNATURE, box, concat, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.js';
3
+ import { cellFault } from './cells.js';
3
4
  const utf8 = new TextEncoder();
4
5
  const text = new TextDecoder();
5
6
  // The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
@@ -49,6 +50,8 @@ export async function openAsk(bytes, padlockSecret) {
49
50
  return null; // a name, or the empty ask. never a number, never an object.
50
51
  if (payload.args !== undefined && (payload.args === null || typeof payload.args !== 'object' || Array.isArray(payload.args)))
51
52
  return null; // args are one object, or absent. a string or a list is not an ask.
53
+ if (payload.args !== undefined && cellFault(payload.args, 'args') !== null)
54
+ return null; // and values all the way down: no key named __proto__, no nesting past the bound
52
55
  if (!Number.isSafeInteger(payload.seq) || payload.seq < 1)
53
56
  return null; // her count for this relation. one and up, and a whole number.
54
57
  // The allowance. A whole number, and one that has already run out is not an
@@ -1,4 +1,4 @@
1
- import type { Cells, JsonObject, Silence, Stance, Wanted, Word } from '../being/types.ts';
1
+ import type { Cells, Invitation, JsonObject, Silence, Stance, Wanted, Word } from '../being/types.ts';
2
2
  import { type Bind, type StandingKeys } from './partition.ts';
3
3
  import { type ReplyPayload } from './seal.ts';
4
4
  export type Inside = {
@@ -11,6 +11,9 @@ export type Inside = {
11
11
  closeHeir(heir: string): void;
12
12
  send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
13
13
  instantiate(key: string, className: string): string | null;
14
+ relate(key: string, id: string): Promise<Invitation | null>;
15
+ unmake(key: string): void;
16
+ wrote(): void;
14
17
  };
15
18
  export type SealedInvitation = {
16
19
  ward: string;