@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
@@ -85,18 +85,83 @@ const key32 = (value: Uint8Array, what: string): Uint8Array => {
85
85
  return value;
86
86
  };
87
87
  const pkcs8 = (prefix: Uint8Array, value: Uint8Array, what: string) => concat([prefix, key32(value, what)]);
88
- const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) =>
89
- subtle().importKey('pkcs8', pkcs8(prefix, value, what) as BufferSource, alg, true, uses);
90
- const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) =>
91
- subtle().importKey('raw', key32(value, what) as BufferSource, alg, true, uses);
92
-
93
- // Subtle exports the public half of a private key only through a JWK, where `x` is the 32 raw bytes in base64url.
94
- async function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
95
- const jwk = await subtle().exportKey('jwk', secret);
96
- const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
97
- const out = new Uint8Array(binary.length);
98
- for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
99
- return out;
88
+
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<string, Promise<CryptoKey>>();
112
+ const keep = (id: string, make: () => Promise<CryptoKey>): Promise<CryptoKey> => {
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) imported.delete(imported.keys().next().value!);
131
+ return made;
132
+ };
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: number; bound: number } => ({ held: imported.size, bound: KEYS });
138
+
139
+ const secretKey = (alg: { name: string }, prefix: Uint8Array, value: Uint8Array, what: string, uses: KeyUsage[]) => {
140
+ const bytes = pkcs8(prefix, value, what);
141
+ return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes as BufferSource, alg, true, uses));
142
+ };
143
+ const publicKey = (alg: { name: string }, value: Uint8Array, what: string, uses: KeyUsage[]) => {
144
+ const bytes = key32(value, what);
145
+ return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes as BufferSource, alg, true, uses));
146
+ };
147
+
148
+ // Subtle exports the public half of a private key only through a JWK, where
149
+ // `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
150
+ // never changes, so it is kept beside the key it was read from and goes when
151
+ // the key does.
152
+ const publics = new WeakMap<CryptoKey, Promise<Uint8Array>>();
153
+ function rawPublic(secret: CryptoKey): Promise<Uint8Array> {
154
+ const had = publics.get(secret);
155
+ if (had !== undefined) return had;
156
+ const read = (async () => {
157
+ const jwk = await subtle().exportKey('jwk', secret);
158
+ const binary = atob(jwk.x!.replaceAll('-', '+').replaceAll('_', '/'));
159
+ const out = new Uint8Array(binary.length);
160
+ for (let at = 0; at < binary.length; at += 1) out[at] = binary.charCodeAt(at);
161
+ return out;
162
+ })();
163
+ publics.set(secret, read);
164
+ return read;
100
165
  }
101
166
 
102
167
  export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
@@ -104,13 +169,17 @@ export async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
104
169
  }
105
170
 
106
171
  export type Pair = { secret: Uint8Array; pk: Uint8Array };
172
+ // Both halves are copies. The seed and the public key are kept behind the two
173
+ // caches above, and a pair is handed to whoever asked for it: what she does
174
+ // with the bytes in her hand is hers, and must not reach what the next caller
175
+ // is given.
107
176
  export async function signingPair(seed: Uint8Array): Promise<Pair> {
108
177
  const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
109
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
178
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
110
179
  }
111
180
  export async function sealingPair(seed: Uint8Array): Promise<Pair> {
112
181
  const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
113
- return { secret: Uint8Array.from(seed), pk: await rawPublic(secret) };
182
+ return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
114
183
  }
115
184
 
116
185
  export async function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array> {
package/src/ward/cells.ts CHANGED
@@ -22,40 +22,82 @@ import type { Cells, Json } from '../being/types.ts';
22
22
  // reply from ending a walk in a stack overflow instead of a refusal.
23
23
  export const DEPTH = 64;
24
24
 
25
+ // How many values one written out may come to. Depth alone does not bound
26
+ // what a value costs to keep: one subvalue may sit under two keys, and
27
+ // nesting that forty levels deep is inside the depth bound and is a trillion
28
+ // values once written down. What is walked here is the object graph, which is
29
+ // small; what a harbor keeps is the tree it expands to, which is not. So the
30
+ // walk counts the tree while it proves the graph, and refuses a value nobody
31
+ // could write down. This is a bound on what a being holds, so it is this
32
+ // kit's number and no word of the spec.
33
+ export const BREADTH = 1 << 20;
34
+
35
+ // Text a harbor can write down: every surrogate in a pair. A lone one is a
36
+ // code unit with no character, which JSON escapes and UTF-8 cannot encode at
37
+ // all, so a harbor that keeps strings as bytes loses it and one that keeps
38
+ // the object does not. I-JSON says a string is text, and this is that rule
39
+ // where the value is written rather than where it is read.
40
+ const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
41
+
25
42
  // I-JSON, all the way down. A number that JSON cannot write is not a number
26
43
  // a harbor can keep, a key on the prototype is not a key she wrote, and a
27
44
  // hole in a list, or a key named `__proto__`, is a thing JSON writes one way
28
45
  // and a runtime reads another.
29
- function fault(v: unknown, path: string, seen: Set<object>, depth: number): string | null {
30
- if (v === null || typeof v === 'boolean' || typeof v === 'string') return null;
31
- if (typeof v === 'number') return Number.isFinite(v) ? null : `${path} is ${String(v)}, which no harbor can write down`;
46
+ //
47
+ // A fault is a sentence; anything else is the count of values under this one,
48
+ // itself included. A value already counted is a value already proven, so a
49
+ // graph that shares is walked once per node rather than once per path.
50
+ function walk(v: unknown, path: string, seen: Set<object>, sized: Map<object, number>, depth: number): string | number {
51
+ if (v === null || typeof v === 'boolean') return 1;
52
+ if (typeof v === 'string') return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
53
+ if (typeof v === 'number') {
54
+ if (!Number.isFinite(v)) return `${path} is ${String(v)}, which no harbor can write down`;
55
+ // Minus zero is a number JSON writes as `0` and reads back as `0`. A
56
+ // harbor that keeps values would hand her the sign she wrote and one that
57
+ // writes JSON would not, and she is never told which she is standing in.
58
+ return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
59
+ }
32
60
  if (typeof v !== 'object') return `${path} is a ${typeof v}, which is not a value`;
33
61
  if (seen.has(v)) return `${path} refers back to itself`;
62
+ const counted = sized.get(v);
63
+ if (counted !== undefined) return counted;
34
64
  if (depth >= DEPTH) return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
35
65
  seen.add(v);
36
66
  try {
67
+ let size = 1;
68
+ const under = (where: string, x: unknown): string | null => {
69
+ const r = walk(x, where, seen, sized, depth + 1);
70
+ if (typeof r === 'string') return r;
71
+ size += r;
72
+ return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
73
+ };
37
74
  if (Array.isArray(v)) {
38
75
  for (let i = 0; i < v.length; i += 1) {
39
76
  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);
77
+ const f = under(`${path}[${i}]`, v[i]);
41
78
  if (f) return f;
42
79
  }
43
- return null;
80
+ sized.set(v, size);
81
+ return size;
44
82
  }
45
83
  const proto = Object.getPrototypeOf(v);
46
84
  if (proto !== Object.prototype && proto !== null) return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
47
85
  for (const k of Object.keys(v)) {
48
86
  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);
87
+ const f = under(`${path}.${k}`, (v as Record<string, unknown>)[k]);
50
88
  if (f) return f;
51
89
  }
52
- return null;
90
+ sized.set(v, size);
91
+ return size;
53
92
  } finally {
54
93
  seen.delete(v);
55
94
  }
56
95
  }
57
96
 
58
- export const cellFault = (v: unknown, path: string): string | null => fault(v, path, new Set(), 0);
97
+ export const cellFault = (v: unknown, path: string): string | null => {
98
+ const out = walk(v, path, new Set(), new Map(), 0);
99
+ return typeof out === 'string' ? out : null;
100
+ };
59
101
 
60
102
  // The three keys at the root of her cells that are the ward's: it writes
61
103
  // them, she reads them, and a write of hers there is refused like a non-value.
@@ -69,6 +111,13 @@ const wrapped = new WeakMap<object, object>();
69
111
  const targets = new WeakMap<object, object>();
70
112
  const guards = new WeakSet();
71
113
 
114
+ // A name no partition can hold, wherever it appears. It is here beside the
115
+ // guard that refuses it, because the ward refuses it too: a relation named
116
+ // this one would be written into the bind table and then throw on her cells,
117
+ // which is half a relation and a throw where a null is promised. One rule,
118
+ // one place, read by the guard and by the stance.
119
+ export const unkeepable = (k: string): boolean => k === '__proto__';
120
+
72
121
  // A key a write may land on: a string that is not `__proto__`, and not one
73
122
  // of the ward's at the root. A symbol key is a thing JSON never writes.
74
123
  const refuse: (why: string) => never = (why) => {
@@ -76,7 +125,7 @@ const refuse: (why: string) => never = (why) => {
76
125
  };
77
126
  function keyFault(t: object, k: string | symbol, v: unknown, path: string, root: boolean): void {
78
127
  if (typeof k !== 'string') refuse(`${path} takes no symbol key`);
79
- if (k === '__proto__') refuse(`${path}.__proto__ is a key no harbor can keep`);
128
+ if (unkeepable(k)) refuse(`${path}.__proto__ is a key no harbor can keep`);
80
129
  if (root && WARDS.has(k)) refuse(`${path}.${k} is the ward's to write`);
81
130
  // A list grows by one at its end, or it has holes JSON cannot write. A
82
131
  // push sets the slot at its length and then the length: both pass. A
@@ -138,4 +187,4 @@ function guard<T extends object>(target: T, path: string, wrote: () => void, roo
138
187
  export const guardCells = (cells: Cells, wrote: () => void = () => {}): Cells => guard(cells as unknown as Record<string, Json>, 'cells', wrote, true) as unknown as Cells;
139
188
  // The cells behind the guard, for the ward alone: the one writer of the
140
189
  // three keys that are its own.
141
- export const unguarded = (cells: Cells): Cells => (targets.get(cells as unknown as object) ?? cells) as Cells;
190
+ export const unguarded = (cells: Cells): Cells => (targets.get(cells) ?? cells) as Cells;
package/src/ward/door.ts CHANGED
@@ -22,10 +22,13 @@ export type Judged = { bytes: Uint8Array; heard: boolean };
22
22
  const SILENCE: ReplyPayload = { silence: true };
23
23
  const said = (quo: DoorWord): ReplyPayload => ({ quo });
24
24
 
25
- // One arrival at one being, already named. Catches every throw. `bound` says
26
- // whether the asker is a key this door holds: she hears `threw`; a stranger
27
- // at the public being hears silence, because her insides are hers.
28
- export async function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
25
+ // One arrival at one being, already named: the three choices, D11, D12 and
26
+ // D13, and nothing else. Catches every throw. `bound` says whether the asker
27
+ // is a key this door holds, or the ward's own owner: she hears `threw`; a
28
+ // stranger at the public being hears silence, because her insides are hers.
29
+ // Every caller that reaches a being's answer goes through here, the owner's
30
+ // `ask` included, so the three choices are written once.
31
+ export async function answered(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
29
32
  const threw = bound ? said('threw') : SILENCE;
30
33
  let out: Awaited<ReturnType<BeingLike['answer']>>;
31
34
  try {
@@ -44,10 +47,18 @@ export async function arrive(door: Door, asker: Asker, method: string | undefine
44
47
  // said, or the door itself would fail to write her reply after the number
45
48
  // was spent. It is threw, like a word out of her.
46
49
  if (cellFault(out, 'answer') !== null) return threw;
47
- if (method === undefined) return { object: out, seen: null };
48
- // The digest rides along, it is not the answer. She has already answered:
49
- // a describe that will not run costs the digest, and nothing else.
50
- return { object: out, seen: await seen(door, asker) };
50
+ return { object: out, seen: null };
51
+ }
52
+
53
+ // What crosses the wire: her answer, with her digest for this asker beside
54
+ // it. The digest rides along, it is not the answer. She has already
55
+ // answered: a describe that will not run costs the digest, and nothing else.
56
+ // The owner does not take one, because the owner asks for a describe when it
57
+ // wants one and nothing is sealed on its behalf.
58
+ export async function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
59
+ const reply = await answered(door, asker, method, args, bound);
60
+ if (method === undefined || !('object' in reply)) return reply;
61
+ return { object: reply.object, seen: await seen(door, asker) };
51
62
  }
52
63
 
53
64
  // Her digest for this asker, or null when her describe threw or fell silent.
@@ -55,7 +66,14 @@ export async function arrive(door: Door, asker: Asker, method: string | undefine
55
66
  export async function seen(door: Door, asker: Asker): Promise<string | null> {
56
67
  try {
57
68
  const bp = await door.being.answer(asker);
58
- return isSilence(bp) || isWord(bp) ? null : await digest(bp);
69
+ if (isSilence(bp) || isWord(bp)) return null;
70
+ // Held to the same rule as her answer, and for the same reason plus one:
71
+ // the digest is a walk, and a walk over a graph that shares or turns back
72
+ // on itself is not a digest but a hang or a throw. A blueprint that could
73
+ // not cross is a blueprint with no digest, which is what she has when her
74
+ // describe says nothing.
75
+ if (cellFault(bp, 'blueprint') !== null) return null;
76
+ return await digest(bp);
59
77
  } catch {
60
78
  return null;
61
79
  }
@@ -1,8 +1,31 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
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
- // why the ward itself knows no runtime.
5
- import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
2
+ // The ground. The one object a harbor passes a ward at birth: seven things
3
+ // the ward uses. Everything a runtime differs on arrives here, which is why
4
+ // the ward itself knows no runtime.
5
+ import type { Stance, BeingLike, BeingClass, Invitation } from '../being/types.ts';
6
+
7
+ // What the device lends this ward's beings, by name. What a box can do is
8
+ // beings, in a ward its harbor booted and roots, and this is how a being of
9
+ // another ward comes to hold a standing at one: the harbor asks its own root
10
+ // to invite on that being, and hands the invitation to the taker.
11
+ //
12
+ // The taker is the ward's half, and it is inside this call rather than after
13
+ // it so that nothing half-lives. The harbor minted, so the harbor is the only
14
+ // one who can unmint: a ward that could not knock, or could not take, says so
15
+ // by answering false, and the root that minted removes the occupant it made.
16
+ // A lend that failed leaves no heir open at that being and no relation bound
17
+ // to a ward that does not hold it, which is the promise `boot` keeps with
18
+ // `unmake` and the same promise here.
19
+ //
20
+ // False is every kind of no, and they are one answer because a being would do
21
+ // nothing different for any of them: this harbor lends nothing, or nothing of
22
+ // that name, or nothing of that name to this ward, or the ward did not take
23
+ // what was minted.
24
+ //
25
+ // The invitation never reaches the being. It is the device's capability, and
26
+ // a value she could copy is one she could hand to anyone, so it lives in this
27
+ // call and nowhere else.
28
+ export type Lend = (name: string, take: (invitation: Invitation) => Promise<boolean>) => Promise<boolean>;
6
29
 
7
30
  export type Ground = {
8
31
  seed: string | Uint8Array; // the ward derives its pk from it and nothing else
@@ -20,19 +43,24 @@ export type Ground = {
20
43
  // undefined is a promise, not a shrug: no door was reached, and nothing was
21
44
  // delivered. The ward hands it to a being as unreached, which is the one
22
45
  // answer that says asking again is safe, so a harbor may only return it
23
- // when it knows the bytes never arrived no reach for that pk, a socket
46
+ // when it knows the bytes never arrived: no reach for that pk, a socket
24
47
  // that would not open, a link that is down.
25
48
  //
26
49
  // A harbor that sent the bytes and then gave up waiting knows no such
27
50
  // thing: the far door may have heard and be working still. It must not
28
51
  // answer at all in that case. The ward bounds every ask itself, and an
29
- // answer that never comes is silence, which promises nothing. So a harbor
30
- // may hold a shorter patience than the ward's for its own reasons — a
31
- // socket it wants back, a queue it will not grow and the two bounds never
32
- // need to read each other: whichever ends first ends the ask, and each says
33
- // only what it can honestly say.
52
+ // answer that never comes inside that bound is `late`, which promises
53
+ // nothing either way. So a harbor may hold a shorter patience than the
54
+ // ward's for its own reasons, a socket it wants back or a queue it will
55
+ // not grow, and the two bounds never need to read each other: whichever
56
+ // ends first ends the ask, and each says only what it can honestly say.
34
57
  carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
35
58
  random(n: number): Uint8Array; // entropy. every key a ward mints is drawn from it
59
+ // A standing at one of the harbor's own beings, by the name that harbor
60
+ // knows it under. Built per ward, so which ward may ask for which name is
61
+ // the harbor's own decision and a stranger's ward is lent nothing. A harbor
62
+ // with nothing to lend leaves it out, and every name answers false.
63
+ lend?: Lend;
36
64
  };
37
65
 
38
66
  // What a ward hands back. Two pointers. The door answers bytes, always, and
@@ -46,7 +74,7 @@ export type WardPointers = {
46
74
  };
47
75
 
48
76
  // ---- what every harbor builds a ground out of. Three pieces, because every
49
- // harbor in this tree writes the same three and the spec keeps the harbors
77
+ // harbor in this tree writes the same three and the protocol keeps the harbors
50
78
  // themselves apart: what a memory harbor and a real one differ on is the
51
79
  // route and the store, and nothing here. A second kit writes its own harbor
52
80
  // and may write these again; they are convenience, never contract.
package/src/ward/heirs.ts CHANGED
@@ -103,6 +103,12 @@ export class Heirs {
103
103
  return;
104
104
  }
105
105
  if (by === h.announced) 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) h.announced = next;
107
113
  }
108
114
  }
package/src/ward/index.ts CHANGED
@@ -1,11 +1,11 @@
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.ts';
4
- export type { Ground, WardPointers } from './ground.ts';
4
+ export type { Ground, WardPointers, Lend } from './ground.ts';
5
5
  export type { Partition, Heir, Bind, StandingKeys } from './partition.ts';
6
- // The two bounds on what a partition may hold, for a kit that must keep the
7
- // same ones and for a test that pins them.
8
- export { GONE, MINTED } from './partition.ts';
6
+ // The three bounds on what a partition may hold, for a kit that must keep
7
+ // the same ones and for a test that pins them.
8
+ export { GONE, MINTED, KNOCKS } from './partition.ts';
9
9
  export type { AskPayload, ReplyPayload } from './seal.ts';
10
10
  // The seal and the arithmetic, for a kit in another language to check its
11
11
  // bytes against, and for tests that speak to a door directly.
package/src/ward/owner.ts CHANGED
@@ -3,17 +3,17 @@
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.ts';
12
12
  import { OWNER } from '../being/types.ts';
13
13
  import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
14
14
  import { put } from './partition.ts';
15
- import { seen } from './door.ts';
16
- import { within, DEFAULT, LATE } from './allowance.ts';
15
+ import { seen, answered } from './door.ts';
16
+ import { within, allow, DEFAULT, LATE } from './allowance.ts';
17
17
  import type { Resident } from './ward.ts';
18
18
 
19
19
  export type OwnerSide = {
@@ -39,6 +39,11 @@ export const OWNER_ASKS: Ask[] = [
39
39
  },
40
40
  { 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'] } },
41
41
  { 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'] } },
42
+ {
43
+ name: 'ask',
44
+ 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',
45
+ input: { type: 'object', properties: { being: str, method: str, args: { type: 'object' }, wanted: { type: 'object', properties: { time: { type: 'number' } } } } },
46
+ },
42
47
  ];
43
48
 
44
49
  export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | undefined, args: JsonObject): Promise<Json> {
@@ -92,7 +97,7 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
92
97
  const notes = args.notes;
93
98
  const door = w.doors.get(being);
94
99
  if (!door) return { error: 'no such being' };
95
- return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes as JsonObject) : undefined);
100
+ return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes) : undefined);
96
101
  }
97
102
  if (method === 'knock') {
98
103
  const b = args.being as string | { boot: unknown; key: unknown };
@@ -121,7 +126,7 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
121
126
  const out = await door.stance.standings.knock(inv, args.method as string | undefined, (args.args as JsonObject) ?? {}, args.wanted as Wanted | undefined);
122
127
  if (isSilence(out)) return { error: 'silence' };
123
128
  if (isWord(out)) return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
124
- return { taken: await door.stance.standings.take(id, inv), answer: out as Json };
129
+ return { taken: await door.stance.standings.take(id, inv), answer: out };
125
130
  }
126
131
  if (method === 'remove') {
127
132
  // The mirror of knock: a relation out of a being, by id, occupant or
@@ -152,6 +157,36 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
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) return { error: 'no such being' };
169
+ // A method is a name or it is the empty ask. Coerced instead of checked,
170
+ // a shape passed where a name belongs becomes a string she never declared,
171
+ // and she would answer `unknown ask` to something nobody asked.
172
+ if (args.method !== undefined && typeof args.method !== 'string') return { error: 'an ask is named by a word' };
173
+ // No being named is the in-process twin of bytes for nobody: the public
174
+ // being, asked as nobody, unbound, so a throw of hers is silence. Her
175
+ // insides are not a stranger's to read, and a device serving her to
176
+ // strangers must hear what a stranger hears and not one word more.
177
+ const key = being ?? w.publicKey();
178
+ const door = key === null ? undefined : w.doors.get(key);
179
+ if (!door || door.key === w.pk) return { error: 'no such being' };
180
+ const bound = being !== null;
181
+ // The owner is a caller like any other: wanted says what this ask may
182
+ // spend, and saying nothing is the ward's default. A wait that ran out is
183
+ // late, the same word a being would hear, said as the object an owner hears.
184
+ const out = await within(allow(args.wanted as Wanted | undefined).time, answered(door, bound ? { id: OWNER } : {}, args.method, (args.args as JsonObject) ?? {}, bound));
185
+ if (out === LATE) return { error: 'late' };
186
+ if ('quo' in out) return { error: out.quo };
187
+ if (!('object' in out)) return { error: 'silence' };
188
+ return out.object;
189
+ }
155
190
  return { error: 'unknown ask' };
156
191
  }
157
192
 
@@ -166,7 +201,7 @@ async function describe(w: OwnerSide): Promise<Json> {
166
201
  // answers its owner: the harbor learns the pk by this very ask at boot, so
167
202
  // an unbounded wait here is a ward no restart can bring back.
168
203
  const asked = [...w.doors].filter(([key]) => key !== w.pk);
169
- const digests = await Promise.all(asked.map(async ([, door]) => (await within(DEFAULT.time, seen(door, { id: OWNER }))) as string | null | typeof LATE));
204
+ const digests = await Promise.all(asked.map(async ([, door]) => (await within(DEFAULT.time, seen(door, { id: OWNER })))));
170
205
  for (const [i, [key, door]] of asked.entries()) {
171
206
  const d = digests[i];
172
207
  put(beings, key, { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d === LATE ? null : d });
package/src/ward/seal.ts CHANGED
@@ -37,6 +37,15 @@ export type WardKey = { sign: Uint8Array; padlock: Uint8Array; signPk: Uint8Arra
37
37
  const WARD_SIGN = new TextEncoder().encode('quo-ward-sign');
38
38
  const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
39
39
 
40
+ // The one size in Quo: one mebibyte of bytes each way. It is read before
41
+ // anything is opened, so bytes above it are never decrypted, never parsed and
42
+ // never allocated against. Refusing them is bytes that said nothing, which is
43
+ // already silence, so there is no tenth word and no new door case. Breadth
44
+ // costs bytes, so this one number bounds every other breadth on the wire, and
45
+ // what a being holds is not the protocol's business. An ask is a message and
46
+ // not a file; what is larger is asked for in pieces.
47
+ export const SIZE = 1024 * 1024;
48
+
40
49
  // The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
41
50
  //
42
51
  // One seed, two curves, and each secret derived from it under its own label.
@@ -45,7 +54,7 @@ const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
45
54
  // designs and no separation at all: one secret would be doing two jobs with
46
55
  // nothing said about it, and a second kit would have to reproduce a
47
56
  // construction nobody named. HKDF-SHA-256 under a label is the separation
48
- // said out loud, and it is what `vectors/framing.json` pins.
57
+ // said out loud, and it is what `protocol/vectors/framing.json` pins.
49
58
  //
50
59
  // Bytes are key material and text is not. A seed handed in as bytes of the
51
60
  // key length is taken as it stands, which is what a harbor mints; anything
@@ -101,7 +110,7 @@ export async function sealAsk(to: string | null, payload: AskPayload, signer: Ui
101
110
  // Verification is the door's own step, because only the door knows which key may speak.
102
111
  export async function openAsk(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<{ to: string | null; payload: AskPayload; body: Uint8Array; signature: Uint8Array; ephemeralPk: Uint8Array } | null> {
103
112
  try {
104
- if (!(bytes instanceof Uint8Array) || bytes.length < 1) return null;
113
+ if (!(bytes instanceof Uint8Array) || bytes.length < 1 || bytes.length > SIZE) return null;
105
114
  const inside = await unbox(bytes, padlockSecret);
106
115
  if (inside.length <= SIGNATURE) return null;
107
116
  const body = inside.subarray(0, inside.length - SIGNATURE);
@@ -146,7 +155,7 @@ export async function sealReply(reply: ReplyPayload, ephemeralPk: Uint8Array, wa
146
155
  // throw or an absent value for what a stranger wrote.
147
156
  export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, signPk: Uint8Array): Promise<ReplyPayload | null> {
148
157
  try {
149
- if (!(bytes instanceof Uint8Array)) return null;
158
+ if (!(bytes instanceof Uint8Array) || bytes.length > SIZE) return null;
150
159
  const inside = await unbox(bytes, ephemeralSecret);
151
160
  if (inside.length <= SIGNATURE) return null;
152
161
  const body = inside.subarray(0, inside.length - SIGNATURE);