@quo-systems/quo 0.2.15 → 0.2.17

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.
@@ -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']);
@@ -4,7 +4,7 @@ export type Ground = {
4
4
  seed: string | Uint8Array;
5
5
  memory: Record<string, unknown>;
6
6
  instantiate(className: string, stance: Stance): BeingLike | null;
7
- wrote?: () => void;
7
+ wrote?: (row: string) => void;
8
8
  carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
9
9
  random(n: number): Uint8Array;
10
10
  lend?: Lend;
@@ -58,3 +58,7 @@ export declare const put: <T>(rec: Record<string, T>, key: string, value: T) =>
58
58
  export declare const drop: (rec: Record<string, unknown>, key: string) => void;
59
59
  export declare const emptyCells: () => Cells;
60
60
  export declare const emptyBind: () => Bind;
61
+ export declare const HEAD = "";
62
+ export declare const rowOf: (p: Partition, row: string) => Record<string, unknown> | undefined;
63
+ export declare const rowsIn: (p: Record<string, unknown>) => string[];
64
+ export declare const fromRows: (rows: Record<string, Record<string, unknown>>) => Record<string, unknown>;
@@ -167,3 +167,56 @@ export const drop = (rec, key) => {
167
167
  };
168
168
  export const emptyCells = () => ({ standings: {}, occupants: {} });
169
169
  export const emptyBind = () => ({ standings: {}, occupants: {}, knocks: {}, answered: {}, minted: [] });
170
+ // ---- the partition in rows, for a store that keeps it
171
+ //
172
+ // A ward hands a harbor one object, so a being who writes one cell has every
173
+ // other being in her ward written down beside her. What a being holds is the
174
+ // one thing here that grows without limit, so she is a row: her cells and her
175
+ // bind table, under her key. Everything else is one head row, and every part
176
+ // of it is bounded already, one record per occupant and two lists that drop
177
+ // their oldest.
178
+ //
179
+ // The split is here, where the shape is, and not in the harbor, which keeps
180
+ // what it is given and reads none of it. A store is handed the rows that
181
+ // moved; what it does with that is its own, and one that writes every row is
182
+ // as correct as one that writes the few.
183
+ export const HEAD = ''; // the row that is not a being: no being's key is empty
184
+ // One row's value: a being, or the head, which is everything the partition
185
+ // holds that is not filed under a being. The head is read as what is left
186
+ // rather than as a list of names, so a store keeps whatever it was handed and
187
+ // hands the same thing back, and a field this file grows later needs no line
188
+ // in a store to survive a round trip.
189
+ //
190
+ // Undefined where the key names nobody, which is a row the store drops.
191
+ export const rowOf = (p, row) => {
192
+ const all = p;
193
+ if (row === HEAD)
194
+ return Object.fromEntries(Object.entries(all).filter(([k]) => k !== 'beings' && k !== 'bind'));
195
+ const cells = at(p.beings ?? {}, row);
196
+ return cells === undefined ? undefined : { cells, bind: at(p.bind ?? {}, row) ?? emptyBind() };
197
+ };
198
+ export const rowsIn = (p) => [HEAD, ...Object.keys(p.beings ?? {})];
199
+ // The partition back from its rows. A head that is absent is a partition
200
+ // nobody has written, and `open` makes an empty one of it.
201
+ export const fromRows = (rows) => {
202
+ const out = { ...(rows[HEAD] ?? {}) };
203
+ const beings = {};
204
+ const bind = {};
205
+ let any = Object.keys(out).length > 0;
206
+ for (const [row, value] of Object.entries(rows)) {
207
+ if (row === HEAD)
208
+ continue;
209
+ put(beings, row, value.cells);
210
+ put(bind, row, value.bind);
211
+ any = true;
212
+ }
213
+ // Nothing kept is nothing handed back. Rows that say only that there are no
214
+ // rows must come back as the empty memory they are, because `open` reads an
215
+ // empty one as a ward's first breath and anything else as a partition
216
+ // somebody wrote, which must then say which version wrote it.
217
+ if (!any)
218
+ return {};
219
+ out.beings = beings;
220
+ out.bind = bind;
221
+ return out;
222
+ };
package/dist/ward/seal.js CHANGED
@@ -26,7 +26,7 @@ export const SIZE = 1024 * 1024;
26
26
  // designs and no separation at all: one secret would be doing two jobs with
27
27
  // nothing said about it, and a second kit would have to reproduce a
28
28
  // construction nobody named. HKDF-SHA-256 under a label is the separation
29
- // said out loud, and it is what `vectors/framing.json` pins.
29
+ // said out loud, and it is what `protocol/vectors/framing.json` pins.
30
30
  //
31
31
  // Bytes are key material and text is not. A seed handed in as bytes of the
32
32
  // key length is taken as it stands, which is what a harbor mints; anything
package/dist/ward/ward.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // leaves, opens every one that arrives.
7
7
  import { silence, isSilence, unreached, isWord, word } from '../being/silence.js';
8
8
  import { OWNER } from '../being/types.js';
9
- import { at, drop, open, put, emptyBind, emptyCells, MINTED } from './partition.js';
9
+ import { at, drop, open, put, emptyBind, emptyCells, HEAD, MINTED } from './partition.js';
10
10
  import { Heirs } from './heirs.js';
11
11
  import { makeDoor } from './door.js';
12
12
  import { buildStance } from './stance.js';
@@ -38,7 +38,13 @@ class Self {
38
38
  this.key = key;
39
39
  this.pk = key.pk;
40
40
  this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
41
- this.heirs = new Heirs(this.p, () => this.#wrote());
41
+ // Opening is itself a write: a memory nobody has written comes back with
42
+ // a version stamped on it and the ward's own tables under it. A ward that
43
+ // said nothing here would have every being of it kept and the head that
44
+ // says which version they are written under kept by nobody, so the next
45
+ // boot would read a partition of no version and refuse it.
46
+ this.#wrote(HEAD);
47
+ this.heirs = new Heirs(this.p, () => this.#wrote(HEAD));
42
48
  this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
43
49
  this.#boot(this.pk, () => this); // the ward's ward is itself
44
50
  // a restart is silent: every being in the cells is constructed again, unasked.
@@ -93,15 +99,18 @@ class Self {
93
99
  unboot: (key) => this.#unboot(key),
94
100
  setPublic: (key) => {
95
101
  this.p.public = key;
96
- this.#wrote();
102
+ this.#wrote(HEAD);
97
103
  },
98
104
  }, asker, method, args);
99
105
  }
100
106
  // ---- ward functions. on the object. no stance reaches them.
101
- // The partition was written. The harbor is told, and nothing more: what it
102
- // does with the word is its own, and the ward never learns.
103
- #wrote() {
104
- this.g.wrote?.();
107
+ // The partition was written, and which row of it: a being by her key, or
108
+ // the head. The harbor is told, and nothing more: what it does with the
109
+ // word is its own, and the ward never learns. Every write says its row,
110
+ // because a harbor told only that something moved must keep all of it, and
111
+ // a being's row is the one part of a partition that grows without limit.
112
+ #wrote(row) {
113
+ this.g.wrote?.(row);
105
114
  }
106
115
  // The rows no door holds this run: beings whose class threw at birth or
107
116
  // is not the harbor's to give. Key to class.
@@ -139,7 +148,7 @@ class Self {
139
148
  if (!door)
140
149
  return null;
141
150
  unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
142
- this.#wrote();
151
+ this.#wrote(key);
143
152
  return door;
144
153
  }
145
154
  // The inverse of boot, and the only way a being leaves a ward. Her
@@ -177,7 +186,8 @@ class Self {
177
186
  drop(this.p.beings, key);
178
187
  drop(this.p.bind, key);
179
188
  this.doors.delete(key);
180
- this.#wrote();
189
+ this.#wrote(key); // she is gone, and her row goes with her
190
+ this.#wrote(HEAD); // her heirs were closed, and the public being may have been her
181
191
  return [...occupants, ...standings];
182
192
  }
183
193
  // Nothing is written until there is somebody to write it for: a class the
@@ -188,17 +198,17 @@ class Self {
188
198
  // makers: the ground's `instantiate` for a being of the ward, and the ward
189
199
  // itself, which is the first being in its own map and is already made.
190
200
  #boot(key, make) {
191
- const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
201
+ const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote(key));
192
202
  const bind = at(this.p.bind, key) ?? emptyBind();
193
203
  const stance = buildStance({
194
204
  pk: this.pk,
195
205
  // This stance, not merely this key: unboot and boot again under the
196
206
  // same key makes a new being, and the old stance is not hers.
197
207
  live: () => this.doors.get(key)?.stance === stance,
198
- mintKey: (b) => this.#mintKey(b),
208
+ mintKey: (b) => this.#mintKey(b, key),
199
209
  openHeir: (heir, being, id) => this.heirs.open(heir, being, id),
200
210
  closeHeir: (heir) => this.heirs.close(heir),
201
- send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted),
211
+ send: (b, keys, method, args, wanted) => this.#send(b, keys, method, args, wanted, key),
202
212
  // A throw at birth is null to her, not a throw in her method: she asked
203
213
  // for a being and got none, and her own answer is still hers to give.
204
214
  instantiate: (className, k) => {
@@ -215,7 +225,7 @@ class Self {
215
225
  // names there are, which ward may ask for one, and what becomes of one
216
226
  // she did not take, is the harbor's and no word of the ward.
217
227
  lend: async (name, take) => (await this.g.lend?.(name, take)) ?? false,
218
- wrote: () => this.#wrote(),
228
+ wrote: () => this.#wrote(key),
219
229
  }, key, cells, bind);
220
230
  const being = make(stance);
221
231
  if (!being || typeof being.answer !== 'function')
@@ -224,10 +234,10 @@ class Self {
224
234
  put(this.p.bind, key, bind);
225
235
  const door = { key, cells, bind, stance, being };
226
236
  this.doors.set(key, door);
227
- this.#wrote();
237
+ this.#wrote(key);
228
238
  return door;
229
239
  }
230
- async #mintKey(bind) {
240
+ async #mintKey(bind, row) {
231
241
  const k = await beingKey(this.g.random(32));
232
242
  // The last few she minted, and no more. A relation rotates on every ask,
233
243
  // so a list of all of them is a partition that grows for as long as she
@@ -236,7 +246,7 @@ class Self {
236
246
  bind.minted.push(k.pk);
237
247
  if (bind.minted.length > MINTED)
238
248
  bind.minted.splice(0, bind.minted.length - MINTED);
239
- this.#wrote();
249
+ this.#wrote(row);
240
250
  return k;
241
251
  }
242
252
  // One send for every door, the ward's own included. Mine: never leaves.
@@ -244,7 +254,7 @@ class Self {
244
254
  // next, seals to the far ward, opens the reply with the ephemeral secret,
245
255
  // and rotates to the announced key once the far door has answered under
246
256
  // the current one.
247
- async #send(bind, keys, method, args, wanted) {
257
+ async #send(bind, keys, method, args, wanted, row) {
248
258
  // What she asked for, held to what this ward allows. Asking for nothing is
249
259
  // the default, and asking for more than the ceiling is the ceiling: budget
250
260
  // is granted by a ward, never minted by a being.
@@ -255,7 +265,7 @@ class Self {
255
265
  // to rotate to. A standing on her signs with one key for life. Every other
256
266
  // ask announces its next: there is no send that does not.
257
267
  if (keys.next === null && keys.heir !== null)
258
- keys.next = (await this.#mintKey(bind)).seed;
268
+ keys.next = (await this.#mintKey(bind, row)).seed;
259
269
  const next = keys.next === null ? null : (await beingKey(unhex(keys.next))).pk;
260
270
  // Her count for this relation, one higher every call and never reused. The
261
271
  // far door honours each number once. One relation sends one at a time, so
@@ -279,10 +289,14 @@ class Self {
279
289
  return unreached();
280
290
  }
281
291
  const { bytes, ephemeral } = sealed;
282
- // The wait is bounded, and this is the one thing the ward times. A being
283
- // holds three answers and a wait that does not end is none of them: a
284
- // relation that comes back round holds a lane the answer needs, and only a
285
- // bound on the wait can break that. What comes back late is not read.
292
+ // The wait is bounded here, and again around the lane in the stance. Two
293
+ // bounds and not one, because they end two different things: the stance's
294
+ // ends the wait a being is held in, and this one ends the occupancy of the
295
+ // relation's lane. A being holds three answers and a wait that does not
296
+ // end is none of them, and a lane nobody ever leaves is a relation the
297
+ // next ask never reaches. Only a bound on the wire breaks the second, and
298
+ // taking it out would leave one quiet far side holding the lane for good.
299
+ // What comes back late is not read.
286
300
  //
287
301
  // A wait that ran out is `late`, never unreached. Unreached promises
288
302
  // nothing was delivered and is safe to retry; a bound that expired knows
@@ -303,7 +317,7 @@ class Self {
303
317
  keys.current = keys.next; // the far door holds `next` as announced. move to it.
304
318
  keys.next = null;
305
319
  }
306
- this.#wrote(); // the count moved, and the keys may have
320
+ this.#wrote(row); // the count moved, and the keys may have
307
321
  return reply;
308
322
  }
309
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
5
5
  "keywords": [
6
6
  "quo",
@@ -35,13 +35,13 @@
35
35
  "types": "./dist/conformance/index.d.ts",
36
36
  "default": "./dist/conformance/index.js"
37
37
  },
38
- "./vectors/*": "./vectors/*",
38
+ "./protocol/*": "./protocol/*",
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "scripts": {
42
- "build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../papers/SPEC.md SPEC.md && cp ../../papers/quo-kit.md quo-kit.md",
43
- "test": "node --test \"test/*.test.ts\"",
44
- "check:terrain": "node --test \"test/terrain/*.test.ts\"",
42
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../papers/SPEC.md protocol/SPEC.md && cp ../../papers/quo-kit.md quo-kit.md && cp ../../papers/GETTING_STARTED.md GETTING_STARTED.md",
43
+ "check": "node --test \"test/*.test.ts\"",
44
+ "deep": "node --test \"test/terrain/*.test.ts\"",
45
45
  "prepublishOnly": "test \"$QUO_GATED\" = 1 || { echo 'publish from the root, gated once: npm run release:quo' >&2; exit 1; }"
46
46
  },
47
47
  "publishConfig": {
@@ -50,9 +50,9 @@
50
50
  "files": [
51
51
  "dist",
52
52
  "src",
53
- "vectors",
54
- "SPEC.md",
53
+ "protocol",
55
54
  "quo-kit.md",
55
+ "GETTING_STARTED.md",
56
56
  "README.md",
57
57
  "LICENSE",
58
58
  "NOTICE"
@@ -710,6 +710,12 @@ spent, so bytes leave and the door's silence comes back: after take the
710
710
  invitation names the standing and nothing else, so it is exactly as alive as
711
711
  the standing is.
712
712
 
713
+ A lane is released by the bound and by nothing else. An ask that never comes
714
+ back holds its relation's lane, and every later ask on that relation waits
715
+ behind it, so the bound ends an occupancy and not only a wait. A kit that
716
+ bounds where a being is waiting, and not where the lane is held, has a
717
+ relation that one quiet far side closes for good.
718
+
713
719
  ### The public being
714
720
 
715
721
  A ward may have one public being, and no more. She is an ordinary being,
@@ -898,7 +904,8 @@ the default would leave her only the narrowing half, and would give the ward
898
904
  no way to allow one long piece of work without making every ask that
899
905
  patient.
900
906
 
901
- The sender's wait has an end, and this is the one thing the ward times. The
907
+ The sender's wait has an end, and time is the only thing the ward measures:
908
+ no count of doors, no count of bytes, no count of tries. The
902
909
  bound covers the whole of an ask, from the moment she calls: a relation that
903
910
  comes back round on itself is stopped at its own lane, before a byte is
904
911
  sealed, and a bound that watched only the wire would never see it. This is
@@ -1045,9 +1052,14 @@ in pieces, by a being who knows how her own work divides.
1045
1052
 
1046
1053
  The hand to a kit in another language is two things, and they are of two
1047
1054
  kinds. `vectors/` is the byte-level hand: fixed inputs and outputs for
1048
- everything a stranger can observe, the arithmetic, the ward pk, the digest,
1049
- the signed body, the sealed shapes, the invitation, the knock, and the
1050
- frames on a socket and the one request a door takes. A kit reproduces them
1055
+ everything a stranger can observe, in four areas. `arithmetic.json` is the
1056
+ primitives the seal rests on. `framing.json` is the ward pk, the digest, the
1057
+ signed body, the sealed shapes, the invitation and the knock.
1058
+ `wire.json` is the frames on a socket and the one request a door takes.
1059
+ `door.json` is the door's thirteen cases, each one an arrival: the bytes that
1060
+ come in, the bytes that go out, what those bytes open to where a hand holds
1061
+ the lid, and the partition's digest before and after, so `nothing written`
1062
+ is a value a kit checks and never a sentence it reads. A kit reproduces them
1051
1063
  or it is not this protocol. The conformance suite is the behavioural hand,
1052
1064
  and it is a checklist and not a harness: one ward is one runtime and one
1053
1065
  language, so the beings a suite is shown with run only in the ward its kit