@quo-systems/quo 0.2.14 → 0.2.16

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 (62) hide show
  1. package/README.md +37 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +8 -3
  4. package/dist/being/index.d.ts +1 -1
  5. package/dist/being/index.js +1 -1
  6. package/dist/being/types.d.ts +2 -0
  7. package/dist/being/types.js +17 -0
  8. package/dist/conformance/beings.d.ts +41 -0
  9. package/dist/conformance/beings.js +28 -2
  10. package/dist/conformance/index.d.ts +10 -1
  11. package/dist/conformance/index.js +162 -6
  12. package/dist/harbor/core.d.ts +4 -2
  13. package/dist/harbor/core.js +25 -4
  14. package/dist/harbor/index.d.ts +1 -1
  15. package/dist/harbor/memory.d.ts +4 -3
  16. package/dist/harbor/memory.js +13 -4
  17. package/dist/harbor/reach.js +1 -1
  18. package/dist/ward/arithmetic.d.ts +4 -0
  19. package/dist/ward/arithmetic.js +82 -12
  20. package/dist/ward/cells.d.ts +2 -0
  21. package/dist/ward/cells.js +60 -11
  22. package/dist/ward/door.d.ts +1 -0
  23. package/dist/ward/door.js +29 -10
  24. package/dist/ward/ground.d.ts +3 -1
  25. package/dist/ward/ground.js +1 -1
  26. package/dist/ward/heirs.js +8 -1
  27. package/dist/ward/index.d.ts +2 -2
  28. package/dist/ward/index.js +3 -3
  29. package/dist/ward/owner.js +49 -8
  30. package/dist/ward/seal.d.ts +1 -0
  31. package/dist/ward/seal.js +11 -3
  32. package/dist/ward/stance.d.ts +1 -0
  33. package/dist/ward/stance.js +62 -4
  34. package/dist/ward/ward.js +13 -4
  35. package/package.json +7 -6
  36. package/{SPEC.md → protocol/SPEC.md} +367 -537
  37. package/protocol/vectors/door.json +345 -0
  38. package/quo-kit.md +595 -0
  39. package/src/being/being.ts +8 -3
  40. package/src/being/index.ts +1 -1
  41. package/src/being/types.ts +34 -0
  42. package/src/conformance/beings.ts +25 -2
  43. package/src/conformance/estate.ts +9 -9
  44. package/src/conformance/index.ts +204 -7
  45. package/src/conformance/reach.ts +1 -1
  46. package/src/harbor/core.ts +26 -5
  47. package/src/harbor/index.ts +1 -1
  48. package/src/harbor/memory.ts +14 -5
  49. package/src/harbor/reach.ts +1 -1
  50. package/src/ward/arithmetic.ts +83 -14
  51. package/src/ward/cells.ts +59 -10
  52. package/src/ward/door.ts +27 -9
  53. package/src/ward/ground.ts +39 -11
  54. package/src/ward/heirs.ts +7 -1
  55. package/src/ward/index.ts +4 -4
  56. package/src/ward/owner.ts +45 -10
  57. package/src/ward/seal.ts +12 -3
  58. package/src/ward/stance.ts +60 -4
  59. package/src/ward/ward.ts +14 -5
  60. /package/{vectors → protocol/vectors}/arithmetic.json +0 -0
  61. /package/{vectors → protocol/vectors}/framing.json +0 -0
  62. /package/{vectors → protocol/vectors}/wire.json +0 -0
@@ -8,10 +8,12 @@
8
8
  // edge harbor is this over an object's storage. None of them is in this
9
9
  // tree, and every one of them passes the conformance suite untouched.
10
10
  //
11
- // The directory is filled three ways, in this order: the harbor's own
11
+ // The directory is filled four ways, in this order: the harbor's own
12
12
  // doors; a socket a dialer holds to it, bound once the door behind the
13
- // dialer's claim has proved it holds the key, and unbound at close; a hint
14
- // from a link. And one fallback: a dialer with nothing in its
13
+ // dialer's claim has proved it holds the key, and unbound at close; the
14
+ // claims of a listener this harbor dialed, proved the same way and reached
15
+ // through that line; and a hint, a pk at a URL, kept in the store. A hint
16
+ // never displaces a reach proved at a door. And one fallback: a dialer with nothing in its
15
17
  // directory for a pk sends down the socket it holds, because the listener
16
18
  // it dialed is the rendezvous and may hold that pk on another socket. Bytes
17
19
  // that arrive from the wire go to an own door or a held socket and never
@@ -68,15 +70,31 @@ export class Harbor {
68
70
  for (const say of this.announcers)
69
71
  say();
70
72
  }
71
- constructor(store, loader = async () => ({})) {
73
+ // What this device lends the beings of the wards it hosts, by name. One
74
+ // for the harbor, and `lendFor` says which ward may ask it: every ward,
75
+ // unless a terrain says a stranger's ward is lent nothing.
76
+ // Set at birth or once the harbor's own ward is standing, which is the
77
+ // usual order: a terrain puts that ward up first, then boots the rest on a
78
+ // ground that can reach it.
79
+ lend;
80
+ constructor(store, loader = async () => ({}), lend) {
72
81
  this.store = store;
73
82
  this.loader = loader;
83
+ this.lend = lend;
84
+ }
85
+ lendFor(_name, _record) {
86
+ return this.lend;
74
87
  }
75
88
  // Boot every ward the store keeps, and learn the hints. One ward that will
76
89
  // not host, a partition this kit cannot read or a loader that throws, is
77
90
  // named in `unbooted` and takes nobody else down with it.
78
91
  async boot() {
79
92
  for (const name of await this.store.list()) {
93
+ // A ward this harbor already hosts is not booted twice. A terrain that
94
+ // keeps a ward of its own puts it up before this runs, so that a being
95
+ // born in any other ward can lend from her first line.
96
+ if (this.wards.has(name))
97
+ continue;
80
98
  try {
81
99
  const kept = await this.store.load(name);
82
100
  if (kept)
@@ -316,6 +334,9 @@ export class Harbor {
316
334
  random,
317
335
  wrote: () => this.#wrote(name),
318
336
  };
337
+ const lend = this.lendFor(name, kept.record);
338
+ if (lend)
339
+ ground.lend = lend;
319
340
  const saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
320
341
  this.#saving.set(name, saving);
321
342
  // A ward that will not be born leaves nothing behind: a partition of a
@@ -3,5 +3,5 @@ export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './co
3
3
  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
- export type { Ground, WardPointers } from '../ward/ground.ts';
6
+ export type { Ground, WardPointers, Lend } from '../ward/ground.ts';
7
7
  export { maker, entropy, learnPk } from '../ward/ground.ts';
@@ -1,5 +1,5 @@
1
1
  import type { BeingClass, BeingLike } from '../being/types.ts';
2
- import { type Ground, type WardPointers } from '../ward/ground.ts';
2
+ import { type Ground, type WardPointers, type Lend } from '../ward/ground.ts';
3
3
  export type Booted = WardPointers & {
4
4
  pk: string;
5
5
  };
@@ -17,8 +17,9 @@ export declare class MemoryHarbor {
17
17
  readonly partitions: Map<string, Record<string, unknown>>;
18
18
  readonly wards: Map<string, Booted>;
19
19
  readonly objects: WeakMap<object, BeingLike>;
20
+ random: Ground['random'];
20
21
  route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
21
22
  link(harbor: MemoryHarbor): void;
22
- boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted>;
23
- reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted>;
23
+ boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted>;
24
+ reboot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>, lend?: Lend): Promise<Booted>;
24
25
  }
@@ -10,6 +10,12 @@ export class MemoryHarbor {
10
10
  partitions = new Map(); // seed -> memory. the harbor keeps it and reads nothing
11
11
  wards = new Map(); // seed -> pointers
12
12
  objects = new WeakMap(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
13
+ // Where the wards this harbor boots draw their entropy. The device's own
14
+ // by default. A harbor may hand a fixed stream instead, which is what pins
15
+ // bytes: every key a ward mints and every nonce it seals under comes from
16
+ // here, so a ward on a known seed with a known stream answers the same
17
+ // bytes every run, and a corpus of them is a corpus and not a sample.
18
+ random = entropy;
13
19
  // the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
14
20
  async route(farPk, bytes) {
15
21
  if (this.down.has(farPk))
@@ -31,18 +37,21 @@ export class MemoryHarbor {
31
37
  harbor.peers.add(this);
32
38
  }
33
39
  // boot by seed. the harbor gets a door and an ask, and learns the pk by asking.
34
- async boot(seed, Ward, classes) {
40
+ // `lend` is what this device lends the ward's beings, and a test's stand-in
41
+ // for a real terrain's: left out, every name answers null.
42
+ async boot(seed, Ward, classes, lend) {
35
43
  this.partitions.set(seed, {});
36
- return this.reboot(seed, Ward, classes);
44
+ return this.reboot(seed, Ward, classes, lend);
37
45
  }
38
46
  // a restart: same seed, same partition, fresh ward. the ward's files are the harbor's to keep.
39
- async reboot(seed, Ward, classes) {
47
+ async reboot(seed, Ward, classes, lend) {
40
48
  const memory = this.partitions.get(seed);
41
49
  if (!memory)
42
50
  throw new Error(`no partition for ${seed}`);
43
51
  const ground = {
44
52
  seed,
45
53
  memory,
54
+ ...(lend ? { lend } : {}),
46
55
  // One objects map for the whole harbor, not one per ward: every ward
47
56
  // here is in this process and a test reaches any being of any of them.
48
57
  instantiate: maker(this.objects, classes),
@@ -52,7 +61,7 @@ export class MemoryHarbor {
52
61
  const back = await this.route(farPk, new Uint8Array(bytes));
53
62
  return back === undefined ? undefined : new Uint8Array(back);
54
63
  },
55
- random: entropy,
64
+ random: (n) => this.random(n),
56
65
  };
57
66
  const w = await Ward(ground);
58
67
  const booted = { ...w, pk: await learnPk(w) };
@@ -60,7 +60,7 @@ export function request(url) {
60
60
  // side holds and the wire suite it speaks, sent when the socket opens and
61
61
  // whenever the pks change.
62
62
  const ASK = 0, REPLY = 1, NONE = 2;
63
- // The wire suite: which frames these are and which four algorithms seal what
63
+ // The wire suite: which frames these are and which five algorithms seal what
64
64
  // they carry. One value, and it is not negotiated. It rides in the handshake
65
65
  // and never on an ask, so an ask stays bytes nobody can tell from noise, and
66
66
  // a line still learns before it carries anything whether the far side speaks
@@ -7,6 +7,10 @@ export declare function unhex(text: string): Uint8Array;
7
7
  export declare function concat(parts: Uint8Array[]): Uint8Array;
8
8
  export declare function sameBytes(a: Uint8Array, b: Uint8Array): boolean;
9
9
  export declare const smallOrder: (pk: Uint8Array) => boolean;
10
+ export declare const heldKeys: () => {
11
+ held: number;
12
+ bound: number;
13
+ };
10
14
  export declare function sha256(...parts: Uint8Array[]): Promise<Uint8Array>;
11
15
  export type Pair = {
12
16
  secret: Uint8Array;
@@ -86,27 +86,97 @@ const key32 = (value, what) => {
86
86
  return value;
87
87
  };
88
88
  const pkcs8 = (prefix, value, what) => concat([prefix, key32(value, what)]);
89
- const secretKey = (alg, prefix, value, what, uses) => subtle().importKey('pkcs8', pkcs8(prefix, value, what), alg, true, uses);
90
- const publicKey = (alg, value, what, uses) => subtle().importKey('raw', key32(value, what), alg, true, uses);
91
- // Subtle exports the public half of a private key only through a JWK, where `x` is the 32 raw bytes in base64url.
92
- async function rawPublic(secret) {
93
- const jwk = await subtle().exportKey('jwk', secret);
94
- const binary = atob(jwk.x.replaceAll('-', '+').replaceAll('_', '/'));
95
- const out = new Uint8Array(binary.length);
96
- for (let at = 0; at < binary.length; at += 1)
97
- out[at] = binary.charCodeAt(at);
98
- return out;
89
+ // Importing a key is the most expensive thing on the path of an ask, and most
90
+ // of the imports are the same key again: a ward signs every reply with the one
91
+ // key, opens every ask with the one padlock, and verifies a relation under the
92
+ // key it verified it under last time. Measured over a round trip, fifteen of
93
+ // the twenty-five imports were bytes already imported once.
94
+ //
95
+ // So an imported key is kept, by the bytes it was imported from. A CryptoKey
96
+ // cannot be changed once it exists, so handing the same one out twice is
97
+ // handing out what a second import would have built. Nothing here is a
98
+ // decision a peer can see: two wards that cache differently, or not at all,
99
+ // speak the same bytes.
100
+ //
101
+ // It is bounded, and that is not a detail. A relation mints a fresh key on
102
+ // every ask, so a ward that talked all day would otherwise hold a key for
103
+ // every ask it ever made. Past the bound the least recently used goes, which
104
+ // is the key of a relation that has fallen quiet, and importing it again
105
+ // costs what it cost the first time.
106
+ //
107
+ // The secret keys in here are the ones the partition already holds in this
108
+ // process, as seeds. The cache is another shape of what the ward is already
109
+ // standing on, and never a second place a secret comes from.
110
+ const KEYS = 512;
111
+ const imported = new Map();
112
+ const keep = (id, make) => {
113
+ const had = imported.get(id);
114
+ if (had !== undefined) {
115
+ imported.delete(id); // and set again below: the most recently used goes last
116
+ imported.set(id, had);
117
+ return had;
118
+ }
119
+ const made = make();
120
+ // A key that would not import is not kept: the next call asks subtle again
121
+ // and hears the same refusal, rather than reading one this cache remembered.
122
+ // Node takes any thirty-two bytes as a public key and finds out at verify,
123
+ // so nothing here reaches this line; a terrain that checks the point at the
124
+ // import does, and a refusal it remembered would be a relation killed for
125
+ // good by one bad arrival.
126
+ made.catch(() => imported.delete(id));
127
+ imported.set(id, made);
128
+ // One in, at most one out: a map keeps what was put in the order it was put,
129
+ // so the first key it names is the one used longest ago.
130
+ if (imported.size > KEYS)
131
+ imported.delete(imported.keys().next().value);
132
+ return made;
133
+ };
134
+ // How many imported keys are held, and the bound they are held under. Nothing
135
+ // in the ward reads either: they are here to be looked at, and for the suite
136
+ // that holds the bound to what it says.
137
+ export const heldKeys = () => ({ held: imported.size, bound: KEYS });
138
+ const secretKey = (alg, prefix, value, what, uses) => {
139
+ const bytes = pkcs8(prefix, value, what);
140
+ return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes, alg, true, uses));
141
+ };
142
+ const publicKey = (alg, value, what, uses) => {
143
+ const bytes = key32(value, what);
144
+ return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes, alg, true, uses));
145
+ };
146
+ // Subtle exports the public half of a private key only through a JWK, where
147
+ // `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
148
+ // never changes, so it is kept beside the key it was read from and goes when
149
+ // the key does.
150
+ const publics = new WeakMap();
151
+ function rawPublic(secret) {
152
+ const had = publics.get(secret);
153
+ if (had !== undefined)
154
+ return had;
155
+ const read = (async () => {
156
+ const jwk = await subtle().exportKey('jwk', secret);
157
+ const binary = atob(jwk.x.replaceAll('-', '+').replaceAll('_', '/'));
158
+ const out = new Uint8Array(binary.length);
159
+ for (let at = 0; at < binary.length; at += 1)
160
+ out[at] = binary.charCodeAt(at);
161
+ return out;
162
+ })();
163
+ publics.set(secret, read);
164
+ return read;
99
165
  }
100
166
  export async function sha256(...parts) {
101
167
  return new Uint8Array(await subtle().digest('SHA-256', concat(parts)));
102
168
  }
169
+ // Both halves are copies. The seed and the public key are kept behind the two
170
+ // caches above, and a pair is handed to whoever asked for it: what she does
171
+ // with the bytes in her hand is hers, and must not reach what the next caller
172
+ // is given.
103
173
  export async function signingPair(seed) {
104
174
  const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
105
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
175
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
106
176
  }
107
177
  export async function sealingPair(seed) {
108
178
  const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
109
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
179
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
110
180
  }
111
181
  export async function sign(message, secret) {
112
182
  const key = await secretKey(ED, ED_SECRET, secret, 'secret', ['sign']);
@@ -1,5 +1,7 @@
1
1
  import type { Cells } from '../being/types.ts';
2
2
  export declare const DEPTH = 64;
3
+ export declare const BREADTH: number;
3
4
  export declare const cellFault: (v: unknown, path: string) => string | null;
5
+ export declare const unkeepable: (k: string) => boolean;
4
6
  export declare const guardCells: (cells: Cells, wrote?: () => void) => Cells;
5
7
  export declare const unguarded: (cells: Cells) => Cells;
@@ -2,32 +2,71 @@
2
2
  // and a peer chooses the depth of what she answers; a bound keeps a hostile
3
3
  // reply from ending a walk in a stack overflow instead of a refusal.
4
4
  export const DEPTH = 64;
5
+ // How many values one written out may come to. Depth alone does not bound
6
+ // what a value costs to keep: one subvalue may sit under two keys, and
7
+ // nesting that forty levels deep is inside the depth bound and is a trillion
8
+ // values once written down. What is walked here is the object graph, which is
9
+ // small; what a harbor keeps is the tree it expands to, which is not. So the
10
+ // walk counts the tree while it proves the graph, and refuses a value nobody
11
+ // could write down. This is a bound on what a being holds, so it is this
12
+ // kit's number and no word of the spec.
13
+ export const BREADTH = 1 << 20;
14
+ // Text a harbor can write down: every surrogate in a pair. A lone one is a
15
+ // code unit with no character, which JSON escapes and UTF-8 cannot encode at
16
+ // all, so a harbor that keeps strings as bytes loses it and one that keeps
17
+ // the object does not. I-JSON says a string is text, and this is that rule
18
+ // where the value is written rather than where it is read.
19
+ const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
5
20
  // I-JSON, all the way down. A number that JSON cannot write is not a number
6
21
  // a harbor can keep, a key on the prototype is not a key she wrote, and a
7
22
  // hole in a list, or a key named `__proto__`, is a thing JSON writes one way
8
23
  // and a runtime reads another.
9
- function fault(v, path, seen, depth) {
10
- if (v === null || typeof v === 'boolean' || typeof v === 'string')
11
- return null;
12
- if (typeof v === 'number')
13
- return Number.isFinite(v) ? null : `${path} is ${String(v)}, which no harbor can write down`;
24
+ //
25
+ // A fault is a sentence; anything else is the count of values under this one,
26
+ // itself included. A value already counted is a value already proven, so a
27
+ // graph that shares is walked once per node rather than once per path.
28
+ function walk(v, path, seen, sized, depth) {
29
+ if (v === null || typeof v === 'boolean')
30
+ return 1;
31
+ if (typeof v === 'string')
32
+ return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
33
+ if (typeof v === 'number') {
34
+ if (!Number.isFinite(v))
35
+ return `${path} is ${String(v)}, which no harbor can write down`;
36
+ // Minus zero is a number JSON writes as `0` and reads back as `0`. A
37
+ // harbor that keeps values would hand her the sign she wrote and one that
38
+ // writes JSON would not, and she is never told which she is standing in.
39
+ return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
40
+ }
14
41
  if (typeof v !== 'object')
15
42
  return `${path} is a ${typeof v}, which is not a value`;
16
43
  if (seen.has(v))
17
44
  return `${path} refers back to itself`;
45
+ const counted = sized.get(v);
46
+ if (counted !== undefined)
47
+ return counted;
18
48
  if (depth >= DEPTH)
19
49
  return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
20
50
  seen.add(v);
21
51
  try {
52
+ let size = 1;
53
+ const under = (where, x) => {
54
+ const r = walk(x, where, seen, sized, depth + 1);
55
+ if (typeof r === 'string')
56
+ return r;
57
+ size += r;
58
+ return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
59
+ };
22
60
  if (Array.isArray(v)) {
23
61
  for (let i = 0; i < v.length; i += 1) {
24
62
  if (!(i in v))
25
63
  return `${path}[${i}] is a hole, which no harbor can write down`;
26
- const f = fault(v[i], `${path}[${i}]`, seen, depth + 1);
64
+ const f = under(`${path}[${i}]`, v[i]);
27
65
  if (f)
28
66
  return f;
29
67
  }
30
- return null;
68
+ sized.set(v, size);
69
+ return size;
31
70
  }
32
71
  const proto = Object.getPrototypeOf(v);
33
72
  if (proto !== Object.prototype && proto !== null)
@@ -35,17 +74,21 @@ function fault(v, path, seen, depth) {
35
74
  for (const k of Object.keys(v)) {
36
75
  if (k === '__proto__')
37
76
  return `${path}.__proto__ is a key no harbor can keep`;
38
- const f = fault(v[k], `${path}.${k}`, seen, depth + 1);
77
+ const f = under(`${path}.${k}`, v[k]);
39
78
  if (f)
40
79
  return f;
41
80
  }
42
- return null;
81
+ sized.set(v, size);
82
+ return size;
43
83
  }
44
84
  finally {
45
85
  seen.delete(v);
46
86
  }
47
87
  }
48
- export const cellFault = (v, path) => fault(v, path, new Set(), 0);
88
+ export const cellFault = (v, path) => {
89
+ const out = walk(v, path, new Set(), new Map(), 0);
90
+ return typeof out === 'string' ? out : null;
91
+ };
49
92
  // The three keys at the root of her cells that are the ward's: it writes
50
93
  // them, she reads them, and a write of hers there is refused like a non-value.
51
94
  const WARDS = new Set(['standings', 'occupants', 'class']);
@@ -56,6 +99,12 @@ const WARDS = new Set(['standings', 'occupants', 'class']);
56
99
  const wrapped = new WeakMap();
57
100
  const targets = new WeakMap();
58
101
  const guards = new WeakSet();
102
+ // A name no partition can hold, wherever it appears. It is here beside the
103
+ // guard that refuses it, because the ward refuses it too: a relation named
104
+ // this one would be written into the bind table and then throw on her cells,
105
+ // which is half a relation and a throw where a null is promised. One rule,
106
+ // one place, read by the guard and by the stance.
107
+ export const unkeepable = (k) => k === '__proto__';
59
108
  // A key a write may land on: a string that is not `__proto__`, and not one
60
109
  // of the ward's at the root. A symbol key is a thing JSON never writes.
61
110
  const refuse = (why) => {
@@ -64,7 +113,7 @@ const refuse = (why) => {
64
113
  function keyFault(t, k, v, path, root) {
65
114
  if (typeof k !== 'string')
66
115
  refuse(`${path} takes no symbol key`);
67
- if (k === '__proto__')
116
+ if (unkeepable(k))
68
117
  refuse(`${path}.__proto__ is a key no harbor can keep`);
69
118
  if (root && WARDS.has(k))
70
119
  refuse(`${path}.${k} is the ward's to write`);
@@ -12,6 +12,7 @@ export type Judged = {
12
12
  bytes: Uint8Array;
13
13
  heard: boolean;
14
14
  };
15
+ export declare function answered(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
15
16
  export declare function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
16
17
  export declare function seen(door: Door, asker: Asker): Promise<string | null>;
17
18
  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
@@ -16,10 +16,13 @@ import { KEY, sealingPair } from './arithmetic.js';
16
16
  import { cellFault } from './cells.js';
17
17
  const SILENCE = { silence: true };
18
18
  const said = (quo) => ({ quo });
19
- // One arrival at one being, already named. Catches every throw. `bound` says
20
- // whether the asker is a key this door holds: she hears `threw`; a stranger
21
- // at the public being hears silence, because her insides are hers.
22
- export async function arrive(door, asker, method, args, bound) {
19
+ // One arrival at one being, already named: the three choices, D11, D12 and
20
+ // D13, and nothing else. Catches every throw. `bound` says whether the asker
21
+ // is a key this door holds, or the ward's own owner: she hears `threw`; a
22
+ // stranger at the public being hears silence, because her insides are hers.
23
+ // Every caller that reaches a being's answer goes through here, the owner's
24
+ // `ask` included, so the three choices are written once.
25
+ export async function answered(door, asker, method, args, bound) {
23
26
  const threw = bound ? said('threw') : SILENCE;
24
27
  let out;
25
28
  try {
@@ -42,18 +45,34 @@ export async function arrive(door, asker, method, args, bound) {
42
45
  // was spent. It is threw, like a word out of her.
43
46
  if (cellFault(out, 'answer') !== null)
44
47
  return threw;
45
- if (method === undefined)
46
- return { object: out, seen: null };
47
- // The digest rides along, it is not the answer. She has already answered:
48
- // a describe that will not run costs the digest, and nothing else.
49
- return { object: out, seen: await seen(door, asker) };
48
+ return { object: out, seen: null };
49
+ }
50
+ // What crosses the wire: her answer, with her digest for this asker beside
51
+ // it. The digest rides along, it is not the answer. She has already
52
+ // answered: a describe that will not run costs the digest, and nothing else.
53
+ // The owner does not take one, because the owner asks for a describe when it
54
+ // wants one and nothing is sealed on its behalf.
55
+ export async function arrive(door, asker, method, args, bound) {
56
+ const reply = await answered(door, asker, method, args, bound);
57
+ if (method === undefined || !('object' in reply))
58
+ return reply;
59
+ return { object: reply.object, seen: await seen(door, asker) };
50
60
  }
51
61
  // Her digest for this asker, or null when her describe threw or fell silent.
52
62
  // The owner's describe reads it the same way for every being of the ward.
53
63
  export async function seen(door, asker) {
54
64
  try {
55
65
  const bp = await door.being.answer(asker);
56
- return isSilence(bp) || isWord(bp) ? null : await digest(bp);
66
+ if (isSilence(bp) || isWord(bp))
67
+ return null;
68
+ // Held to the same rule as her answer, and for the same reason plus one:
69
+ // the digest is a walk, and a walk over a graph that shares or turns back
70
+ // on itself is not a digest but a hang or a throw. A blueprint that could
71
+ // not cross is a blueprint with no digest, which is what she has when her
72
+ // describe says nothing.
73
+ if (cellFault(bp, 'blueprint') !== null)
74
+ return null;
75
+ return await digest(bp);
57
76
  }
58
77
  catch {
59
78
  return null;
@@ -1,4 +1,5 @@
1
- import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
1
+ import type { Stance, BeingLike, BeingClass, Invitation } from '../being/types.ts';
2
+ export type Lend = (name: string, take: (invitation: Invitation) => Promise<boolean>) => Promise<boolean>;
2
3
  export type Ground = {
3
4
  seed: string | Uint8Array;
4
5
  memory: Record<string, unknown>;
@@ -6,6 +7,7 @@ export type Ground = {
6
7
  wrote?: () => void;
7
8
  carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
8
9
  random(n: number): Uint8Array;
10
+ lend?: Lend;
9
11
  };
10
12
  export type WardPointers = {
11
13
  door(bytes: Uint8Array): Promise<{
@@ -1,5 +1,5 @@
1
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
2
+ // harbor in this tree writes the same three and the protocol keeps the harbors
3
3
  // themselves apart: what a memory harbor and a real one differ on is the
4
4
  // route and the store, and nothing here. A second kit writes its own harbor
5
5
  // and may write these again; they are convenience, never contract.
@@ -103,6 +103,13 @@ export class Heirs {
103
103
  }
104
104
  if (by === h.announced)
105
105
  h.current = by;
106
- h.announced = next;
106
+ // A send that announced nothing leaves the spare standing. This kit
107
+ // announces on every send but a public one, which holds no heir and
108
+ // reaches none of this; a kit that skips one is a kit whose own next is
109
+ // still the key it announced last time, and forgetting it here would meet
110
+ // that key with silence and kill a healthy relation. Nothing is dropped
111
+ // that was vouched for until something replaces it.
112
+ if (next !== null)
113
+ h.announced = next;
107
114
  }
108
115
  }
@@ -1,7 +1,7 @@
1
1
  export { Ward } from './ward.ts';
2
- export type { Ground, WardPointers } from './ground.ts';
2
+ export type { Ground, WardPointers, Lend } from './ground.ts';
3
3
  export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
4
- export { GONE, MINTED } from './partition.ts';
4
+ export { GONE, MINTED, KNOCKS } from './partition.ts';
5
5
  export type { AskPayload, ReplyPayload } from './seal.ts';
6
6
  export * as seal from './seal.ts';
7
7
  export * as arithmetic from './arithmetic.ts';
@@ -1,9 +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
+ // The three bounds on what a partition may hold, for a kit that must keep
5
+ // the same ones and for a test that pins them.
6
+ export { GONE, MINTED, KNOCKS } from './partition.js';
7
7
  // The seal and the arithmetic, for a kit in another language to check its
8
8
  // bytes against, and for tests that speak to a door directly.
9
9
  export * as seal from './seal.js';
@@ -3,16 +3,16 @@
3
3
  // is an occupant of the ward itself, invited by the root. The ward is a
4
4
  // being to her owner: the empty ask is her describe, and it carries her asks
5
5
  // with a description and an input each, so a side renders them as it
6
- // renders any being's. Six asks: boot, public, invite, knock, remove and
7
- // unboot, which is boot's inverse and takes her relations with her. The
8
- // knock is a being's knock made for her, and the standing is written into
9
- // her cells under the id the owner gave; remove takes a relation out of her
10
- // by id, the mirror of it.
6
+ // renders any being's. Seven asks: boot, public, invite, knock, remove and
7
+ // unboot, which is boot's inverse and takes her relations with her, and ask,
8
+ // which reaches into a being and asks her. The knock is a being's knock made
9
+ // for her, and the standing is written into her cells under the id the owner
10
+ // gave; remove takes a relation out of her by id, the mirror of it.
11
11
  import { isSilence, isWord, wordOf } from '../being/silence.js';
12
12
  import { OWNER } from '../being/types.js';
13
13
  import { put } from './partition.js';
14
- import { seen } from './door.js';
15
- import { within, DEFAULT, LATE } from './allowance.js';
14
+ import { seen, answered } from './door.js';
15
+ import { within, allow, DEFAULT, LATE } from './allowance.js';
16
16
  const str = { type: 'string' };
17
17
  export const OWNER_ASKS = [
18
18
  { 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'] } },
@@ -25,6 +25,11 @@ export const OWNER_ASKS = [
25
25
  },
26
26
  { name: 'remove', description: 'take a relation out of a being of the ward by id, occupant or standing; on the ward pk it unseats an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
27
27
  { name: 'unboot', description: 'take a being out of the ward, with every relation she holds; her occupants hear removed, and the ward itself is refused', input: { type: 'object', properties: { being: str }, required: ['being'] } },
28
+ {
29
+ name: 'ask',
30
+ description: 'ask a being of the ward, as the owner; with no being it is the public being asked as nobody, which is what a stranger would hear',
31
+ input: { type: 'object', properties: { being: str, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } } },
32
+ },
28
33
  ];
29
34
  export async function ownerAnswer(w, asker, method, args) {
30
35
  // Every name the owner gives is a string, or it is nothing. Coerced instead
@@ -82,7 +87,7 @@ export async function ownerAnswer(w, asker, method, args) {
82
87
  const door = w.doors.get(being);
83
88
  if (!door)
84
89
  return { error: 'no such being' };
85
- return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? notes : undefined);
90
+ return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes) : undefined);
86
91
  }
87
92
  if (method === 'knock') {
88
93
  const b = args.being;
@@ -152,6 +157,42 @@ export async function ownerAnswer(w, asker, method, args) {
152
157
  const removed = w.unboot(being);
153
158
  return removed === null ? { error: 'no such being' } : { unbooted: being, removed };
154
159
  }
160
+ if (method === 'ask') {
161
+ // The owner's other power: to reach into a being and ask her. It is
162
+ // strictly less than unboot, which takes her out with every relation she
163
+ // holds, and it is the third asker of the ward-to-being edge finally
164
+ // filled in by the ward rather than minted by whoever holds the pointer.
165
+ // Bounded, judged and named here, so no side outside the ward ever holds
166
+ // a being's answer and writes the door's discipline again beside it.
167
+ const being = args.being === undefined ? null : named(args.being);
168
+ if (args.being !== undefined && being === null)
169
+ return { error: 'no such being' };
170
+ // A method is a name or it is the empty ask. Coerced instead of checked,
171
+ // a shape passed where a name belongs becomes a string she never declared,
172
+ // and she would answer `unknown ask` to something nobody asked.
173
+ if (args.method !== undefined && typeof args.method !== 'string')
174
+ return { error: 'an ask is named by a word' };
175
+ // No being named is the in-process twin of bytes for nobody: the public
176
+ // being, asked as nobody, unbound, so a throw of hers is silence. Her
177
+ // insides are not a stranger's to read, and a device serving her to
178
+ // strangers must hear what a stranger hears and not one word more.
179
+ const key = being ?? w.publicKey();
180
+ const door = key === null ? undefined : w.doors.get(key);
181
+ if (!door || door.key === w.pk)
182
+ return { error: 'no such being' };
183
+ const bound = being !== null;
184
+ // The owner is a caller like any other: wanted says what this ask may
185
+ // spend, and saying nothing is the ward's default. A wait that ran out is
186
+ // late, the same word a being would hear, said as the object an owner hears.
187
+ const out = await within(allow(args.wanted).time, answered(door, bound ? { id: OWNER } : {}, args.method, args.args ?? {}, bound));
188
+ if (out === LATE)
189
+ return { error: 'late' };
190
+ if ('quo' in out)
191
+ return { error: out.quo };
192
+ if (!('object' in out))
193
+ return { error: 'silence' };
194
+ return out.object;
195
+ }
155
196
  return { error: 'unknown ask' };
156
197
  }
157
198
  // The ward's describe for its owner: its beings, their classes, a digest each,
@@ -6,6 +6,7 @@ export type WardKey = {
6
6
  padlockPk: Uint8Array;
7
7
  pk: string;
8
8
  };
9
+ export declare const SIZE: number;
9
10
  export declare function wardKey(seed: string | Uint8Array): Promise<WardKey>;
10
11
  export declare const wardSignPk: (pk: string) => Uint8Array;
11
12
  export declare const wardPadlock: (pk: string) => Uint8Array;