@quo-systems/quo 0.2.10 → 0.2.12
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.
- package/SPEC.md +141 -22
- package/dist/being/being.js +7 -1
- package/dist/being/digest.js +15 -1
- package/dist/being/index.d.ts +2 -2
- package/dist/being/index.js +2 -2
- package/dist/being/silence.d.ts +2 -0
- package/dist/being/silence.js +12 -0
- package/dist/being/types.d.ts +2 -0
- package/dist/being/types.js +25 -0
- package/dist/conformance/assert.js +40 -6
- package/dist/conformance/beings.d.ts +48 -5
- package/dist/conformance/beings.js +39 -8
- package/dist/conformance/estate.js +109 -18
- package/dist/conformance/index.d.ts +5 -2
- package/dist/conformance/index.js +164 -6
- package/dist/harbor/core.d.ts +3 -2
- package/dist/harbor/core.js +86 -25
- package/dist/harbor/dial.js +28 -13
- package/dist/harbor/index.d.ts +1 -0
- package/dist/harbor/index.js +3 -0
- package/dist/harbor/memory.d.ts +3 -3
- package/dist/harbor/memory.js +6 -12
- package/dist/harbor/reach.js +10 -10
- package/dist/ward/allowance.js +7 -2
- package/dist/ward/arithmetic.d.ts +1 -0
- package/dist/ward/arithmetic.js +11 -3
- package/dist/ward/door.d.ts +2 -2
- package/dist/ward/door.js +14 -5
- package/dist/ward/ground.d.ts +4 -1
- package/dist/ward/ground.js +38 -1
- package/dist/ward/heirs.d.ts +1 -2
- package/dist/ward/heirs.js +13 -14
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +3 -0
- package/dist/ward/owner.d.ts +4 -4
- package/dist/ward/owner.js +36 -19
- package/dist/ward/partition.d.ts +3 -0
- package/dist/ward/partition.js +109 -4
- package/dist/ward/seal.d.ts +1 -0
- package/dist/ward/seal.js +38 -13
- package/dist/ward/stance.d.ts +3 -2
- package/dist/ward/stance.js +48 -20
- package/dist/ward/ward.d.ts +1 -1
- package/dist/ward/ward.js +40 -19
- package/package.json +1 -1
- package/src/being/being.ts +7 -1
- package/src/being/digest.ts +15 -2
- package/src/being/index.ts +2 -2
- package/src/being/silence.ts +14 -0
- package/src/being/types.ts +29 -1
- package/src/conformance/assert.ts +37 -4
- package/src/conformance/beings.ts +41 -10
- package/src/conformance/estate.ts +106 -17
- package/src/conformance/index.ts +187 -12
- package/src/harbor/core.ts +98 -25
- package/src/harbor/dial.ts +41 -14
- package/src/harbor/index.ts +3 -0
- package/src/harbor/memory.ts +8 -14
- package/src/harbor/reach.ts +10 -11
- package/src/ward/allowance.ts +7 -2
- package/src/ward/arithmetic.ts +14 -5
- package/src/ward/door.ts +16 -7
- package/src/ward/ground.ts +43 -1
- package/src/ward/heirs.ts +13 -14
- package/src/ward/index.ts +3 -0
- package/src/ward/owner.ts +45 -26
- package/src/ward/partition.ts +109 -5
- package/src/ward/seal.ts +39 -12
- package/src/ward/stance.ts +53 -23
- package/src/ward/ward.ts +46 -25
- package/vectors/framing.json +16 -10
- package/vectors/wire.json +4 -4
package/dist/harbor/core.js
CHANGED
|
@@ -17,12 +17,16 @@
|
|
|
17
17
|
// that arrive from the wire go to an own door or a held socket and never
|
|
18
18
|
// onward by request or by fallback; that one rule is the rendezvous.
|
|
19
19
|
import { Ward } from '../ward/ward.js';
|
|
20
|
+
import { maker, entropy as random, learnPk } from '../ward/ground.js';
|
|
20
21
|
import { silence } from '../being/silence.js';
|
|
21
22
|
import { request } from './reach.js';
|
|
22
|
-
import { openReply, wardSignPk } from '../ward/seal.js';
|
|
23
|
+
import { isWardPk, openReply, wardSignPk } from '../ward/seal.js';
|
|
23
24
|
import { concat, sealingPair, KEY } from '../ward/arithmetic.js';
|
|
24
25
|
import { within, LATE } from '../ward/allowance.js';
|
|
25
26
|
export const DEFAULT_CODE = 'classes/index.ts';
|
|
27
|
+
// The device's entropy, spelled once. A harbor draws it for a ward's seed at
|
|
28
|
+
// birth, for the lid of every probe it sends, and it hands the same function
|
|
29
|
+
// to every ward it boots as the ground's `random`.
|
|
26
30
|
// How long a probe waits for the door behind a claim. A claim that answers
|
|
27
31
|
// nothing in this time is not bound; the next announce is another chance.
|
|
28
32
|
const PROBE = 10_000;
|
|
@@ -85,6 +89,17 @@ export class Harbor {
|
|
|
85
89
|
for (const [pk, url] of Object.entries(await this.store.hints()))
|
|
86
90
|
this.hint(pk, url);
|
|
87
91
|
}
|
|
92
|
+
// The object a ward writes into, by the name it is kept under: the
|
|
93
|
+
// partition the harbor was handed at boot and keeps so it can save it.
|
|
94
|
+
// Nothing in the harbor reads it, and it is not on `Hosted`, because a
|
|
95
|
+
// side that holds a ward would then hold every seed in it and reach any
|
|
96
|
+
// being past every door. It is here for one reader, the conformance
|
|
97
|
+
// suite, whose census is ward state read straight and whose forgeries are
|
|
98
|
+
// ward state written straight, the way `MemoryHarbor.partitions` is. A
|
|
99
|
+
// side asks the ward.
|
|
100
|
+
partitionOf(name) {
|
|
101
|
+
return this.#saving.get(name)?.memory;
|
|
102
|
+
}
|
|
88
103
|
// ---- saving
|
|
89
104
|
// The ward wrote. Its partition is saved once the line is free, and once
|
|
90
105
|
// for every burst of writes, in the order the writes came.
|
|
@@ -178,10 +193,9 @@ export class Harbor {
|
|
|
178
193
|
// the one it sealed itself, and it reads nothing from it but that it is
|
|
179
194
|
// the silence a door owes such a box, signed by the claimed key.
|
|
180
195
|
async prove(pk, reach) {
|
|
181
|
-
if (
|
|
196
|
+
if (!isWardPk(pk))
|
|
182
197
|
return false;
|
|
183
198
|
try {
|
|
184
|
-
const random = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
185
199
|
const lid = await sealingPair(random(KEY));
|
|
186
200
|
const back = await within(PROBE, reach.carry(pk, concat([lid.pk, random(KEY)])));
|
|
187
201
|
if (back === LATE || back === undefined)
|
|
@@ -213,10 +227,23 @@ export class Harbor {
|
|
|
213
227
|
this.reaches.delete(pk);
|
|
214
228
|
const claimed = [...new Set(pks)].slice(0, ANNOUNCE);
|
|
215
229
|
const prove = async (pk) => {
|
|
216
|
-
if (this.doors.has(pk)
|
|
230
|
+
if (this.doors.has(pk))
|
|
231
|
+
return;
|
|
232
|
+
// A line already held for this pk is not displaced by another line that
|
|
233
|
+
// proves the same pk. A proof says only that the ward is reachable that
|
|
234
|
+
// way, and a relay that forwards to the real ward proves as well as the
|
|
235
|
+
// ward's own line does: taking the binding would add a hop the relay
|
|
236
|
+
// chooses, and can drop. The first held line keeps it until it closes,
|
|
237
|
+
// which unbind says.
|
|
238
|
+
const standing = this.reaches.get(pk);
|
|
239
|
+
if (standing?.held && standing.reach !== reach)
|
|
240
|
+
return;
|
|
241
|
+
if (!(await this.prove(pk, reach)))
|
|
217
242
|
return;
|
|
218
243
|
if (announces.get(reach) !== said)
|
|
219
244
|
return; // a newer announce has spoken since
|
|
245
|
+
if (this.reaches.get(pk)?.held && this.reaches.get(pk).reach !== reach)
|
|
246
|
+
return; // one took it while this was proving
|
|
220
247
|
this.reaches.set(pk, { reach, held });
|
|
221
248
|
};
|
|
222
249
|
for (let i = 0; i < claimed.length; i += PROVING)
|
|
@@ -230,7 +257,7 @@ export class Harbor {
|
|
|
230
257
|
// ---- wards
|
|
231
258
|
// Mint a seed and boot an empty ward under a name: a world born here.
|
|
232
259
|
async create(name, user = '', code = DEFAULT_CODE) {
|
|
233
|
-
const kept = { seed:
|
|
260
|
+
const kept = { seed: random(KEY), partition: {}, record: { pk: '', code, user } };
|
|
234
261
|
await this.store.put(name, kept);
|
|
235
262
|
const hosted = await this.host(name, kept);
|
|
236
263
|
await this.store.record(name, hosted.record);
|
|
@@ -244,19 +271,35 @@ export class Harbor {
|
|
|
244
271
|
const h = this.wards.get(name);
|
|
245
272
|
if (h) {
|
|
246
273
|
await h.save();
|
|
274
|
+
// Gone for whoever still holds the old pointers, whose save closure has
|
|
275
|
+
// this row in hand, and out of the map, which now says what this harbor
|
|
276
|
+
// serves and nothing else.
|
|
247
277
|
const s = this.#saving.get(name);
|
|
248
278
|
if (s)
|
|
249
279
|
s.gone = true;
|
|
280
|
+
this.#saving.delete(name);
|
|
250
281
|
this.wards.delete(name);
|
|
251
282
|
this.doors.delete(h.pk);
|
|
252
283
|
this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
|
|
253
284
|
}
|
|
254
285
|
return this.store.take(name);
|
|
255
286
|
}
|
|
256
|
-
// Put what another harbor dropped, and boot it: same seed, same pk.
|
|
287
|
+
// Put what another harbor dropped, and boot it: same seed, same pk. A ward
|
|
288
|
+
// that will not boot is not adopted. This is the one path where a partition
|
|
289
|
+
// written somewhere else arrives, so it is the one place the ward's own
|
|
290
|
+
// shape check is met, and the name is refused for as long as it is kept:
|
|
291
|
+
// a partition put and left would hold the name against every later try,
|
|
292
|
+
// with a copy of it in a store that can never serve it. It goes back out,
|
|
293
|
+
// and the only copy is the one the caller is still holding.
|
|
257
294
|
async adopt(name, kept) {
|
|
258
295
|
await this.store.put(name, kept);
|
|
259
|
-
|
|
296
|
+
try {
|
|
297
|
+
return await this.host(name, kept);
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
await this.store.take(name);
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
260
303
|
}
|
|
261
304
|
// A ward from what the store keeps: the ground, once, and two pointers.
|
|
262
305
|
async host(name, kept) {
|
|
@@ -266,36 +309,49 @@ export class Harbor {
|
|
|
266
309
|
const ground = {
|
|
267
310
|
seed,
|
|
268
311
|
memory,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (!C)
|
|
273
|
-
return null;
|
|
274
|
-
const obj = new C(stance);
|
|
275
|
-
objects.set(stance.cells, obj);
|
|
276
|
-
return obj;
|
|
277
|
-
},
|
|
312
|
+
// This ward's own classes first, then the harbor's: a ward that names a
|
|
313
|
+
// class of its own is answered with hers.
|
|
314
|
+
instantiate: maker(objects, classes, this.classes),
|
|
278
315
|
carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
|
|
279
|
-
random
|
|
316
|
+
random,
|
|
280
317
|
wrote: () => this.#wrote(name),
|
|
281
318
|
};
|
|
282
319
|
const saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
|
|
283
320
|
this.#saving.set(name, saving);
|
|
284
|
-
|
|
321
|
+
// A ward that will not be born leaves nothing behind: a partition of a
|
|
322
|
+
// shape this kit cannot read throws here, and a name the harbor does not
|
|
323
|
+
// serve must not have a saving row saying it does. Learning the pk is
|
|
324
|
+
// inside this, because a ward that will not say its pk is a ward this
|
|
325
|
+
// harbor cannot route to and is no more born than one that threw.
|
|
326
|
+
let w, pk;
|
|
327
|
+
try {
|
|
328
|
+
w = await Ward(ground);
|
|
329
|
+
pk = await learnPk(w);
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
saving.gone = true;
|
|
333
|
+
this.#saving.delete(name);
|
|
334
|
+
throw e;
|
|
335
|
+
}
|
|
285
336
|
// A save the caller may wait for: what the ward wrote so far is in the
|
|
286
337
|
// store when this resolves, or the fault is counted. After a call through
|
|
287
338
|
// a pointer the store is current, so a restart right after it finds
|
|
288
|
-
// everything; a refusal at the door wrote nothing and saves nothing
|
|
339
|
+
// everything; a refusal at the door wrote nothing and saves nothing, and
|
|
340
|
+
// neither does a read. A ward that wrote nothing has nothing to save, so
|
|
341
|
+
// asking it what it holds is free, which is what makes the ask pointer
|
|
342
|
+
// the way to find out rather than an expensive way.
|
|
289
343
|
const save = async () => {
|
|
290
344
|
if (saving.gone)
|
|
291
345
|
return;
|
|
292
|
-
saving.dirty = true;
|
|
293
346
|
await this.#flush(name);
|
|
294
347
|
};
|
|
295
348
|
const door = async (bytes) => {
|
|
296
349
|
const r = await w.door(bytes);
|
|
297
|
-
|
|
298
|
-
|
|
350
|
+
// The reply waits for the store, so a restart right after it finds
|
|
351
|
+
// what the arrival wrote. Whether a key this door holds spoke is not
|
|
352
|
+
// the test: a stranger at the public being is not heard and her being
|
|
353
|
+
// may still have written, and a refusal wrote nothing whoever made it.
|
|
354
|
+
await save();
|
|
299
355
|
return r;
|
|
300
356
|
};
|
|
301
357
|
const ask = async (method, args) => {
|
|
@@ -305,13 +361,18 @@ export class Harbor {
|
|
|
305
361
|
await save();
|
|
306
362
|
return out;
|
|
307
363
|
};
|
|
308
|
-
|
|
364
|
+
// The objects map is keyed by the cells the ward handed instantiate, so
|
|
365
|
+
// the way back to a being is her cells; the partition is read for that
|
|
366
|
+
// one lookup and for nothing else, and a key with no object is a being
|
|
367
|
+
// who is not here this run. `keys` is what the map holds, so a caller
|
|
368
|
+
// never needs the beings table to find out what to ask for.
|
|
309
369
|
const beings = memory.beings;
|
|
310
370
|
const being = (key) => {
|
|
311
|
-
const cells = beings
|
|
371
|
+
const cells = beings && Object.hasOwn(beings, key) ? beings[key] : undefined;
|
|
312
372
|
return cells ? objects.get(cells) : undefined;
|
|
313
373
|
};
|
|
314
|
-
const
|
|
374
|
+
const keys = () => Object.keys(beings ?? {}).filter((k) => being(k) !== undefined);
|
|
375
|
+
const hosted = { door, ask, pk, name, record: { ...kept.record, pk }, being, keys, save };
|
|
315
376
|
this.wards.set(name, hosted);
|
|
316
377
|
this.doors.set(hosted.pk, door);
|
|
317
378
|
await save();
|
package/dist/harbor/dial.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { REFUSED, Socket } from './reach.js';
|
|
2
2
|
export function dial(harbor, url) {
|
|
3
3
|
const d = { url, socket: null, wake: () => { }, close: () => { } };
|
|
4
|
-
let stopped = false, wait = 1000, timer, line
|
|
4
|
+
let stopped = false, wait = 1000, timer, line, // the line in hand, open or still opening: close() closes it either way
|
|
5
|
+
held; // the one this dialer owns now, and how it gives back what the line took
|
|
5
6
|
const connect = () => {
|
|
6
7
|
timer = undefined; // no wait pending: this is the dial it was waiting for
|
|
7
8
|
if (stopped)
|
|
@@ -15,13 +16,15 @@ export function dial(harbor, url) {
|
|
|
15
16
|
wait = 1000;
|
|
16
17
|
void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
|
|
17
18
|
}, (why) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
shed();
|
|
20
|
+
// A line this dialer already put down. Its close arrives whenever the
|
|
21
|
+
// system gets round to noticing, which on a socket that died under a
|
|
22
|
+
// sleeping device is minutes; by then the line dialed in its place is
|
|
23
|
+
// the one in hand, so this one gives back what it took and dials
|
|
24
|
+
// nothing. Only the line the dialer still holds dials again.
|
|
25
|
+
if (held?.socket !== s)
|
|
26
|
+
return;
|
|
27
|
+
held = undefined;
|
|
25
28
|
// A suite this listener will not speak is not a line that dropped. It
|
|
26
29
|
// will not become speakable by asking again sooner, so the wait goes
|
|
27
30
|
// straight to the ceiling and stays there until it changes its mind.
|
|
@@ -31,6 +34,18 @@ export function dial(harbor, url) {
|
|
|
31
34
|
timer = setTimeout(connect, wait);
|
|
32
35
|
wait = Math.min(wait * 2, 30000);
|
|
33
36
|
});
|
|
37
|
+
// Everything this line took from the harbor, given back. Said when it
|
|
38
|
+
// closes, and when the dialer puts it down without waiting for that.
|
|
39
|
+
const shed = () => {
|
|
40
|
+
harbor.announcers.delete(say);
|
|
41
|
+
harbor.unbind(s);
|
|
42
|
+
if (d.socket === s)
|
|
43
|
+
d.socket = null;
|
|
44
|
+
const at = harbor.fallbacks.indexOf(s);
|
|
45
|
+
if (at !== -1)
|
|
46
|
+
harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
|
|
47
|
+
};
|
|
48
|
+
held = { socket: s, shed };
|
|
34
49
|
harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
|
|
35
50
|
// What this harbor holds, said when the line opens and again whenever it
|
|
36
51
|
// changes. A ward booted or adopted after the handshake is announced on
|
|
@@ -47,13 +62,13 @@ export function dial(harbor, url) {
|
|
|
47
62
|
d.wake = () => {
|
|
48
63
|
if (stopped)
|
|
49
64
|
return;
|
|
50
|
-
if (d.socket)
|
|
51
|
-
return d.socket.announce([...harbor.doors.keys()]);
|
|
52
|
-
if (timer === undefined)
|
|
53
|
-
return;
|
|
54
65
|
clearTimeout(timer);
|
|
55
66
|
timer = undefined;
|
|
56
|
-
wait = 1000;
|
|
67
|
+
wait = 1000; // the device is back, and the wait it froze belongs to the sleep, not to this line
|
|
68
|
+
const old = held;
|
|
69
|
+
held = undefined; // put down before the new one is dialed, so a fallback list never holds the dead line ahead of the live one
|
|
70
|
+
old?.shed();
|
|
71
|
+
old?.socket.close();
|
|
57
72
|
connect();
|
|
58
73
|
};
|
|
59
74
|
d.close = () => {
|
package/dist/harbor/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ 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
6
|
export type { Ground, WardPointers } from '../ward/ground.ts';
|
|
7
|
+
export { maker, entropy, learnPk } from '../ward/ground.ts';
|
package/dist/harbor/index.js
CHANGED
|
@@ -8,3 +8,6 @@ export { Harbor, DEFAULT_CODE } from './core.js';
|
|
|
8
8
|
export { dial } from './dial.js';
|
|
9
9
|
export { MemoryStore, values } from './store.js';
|
|
10
10
|
export { request, Socket, SUITE, REFUSED } from './reach.js';
|
|
11
|
+
// What a harbor builds a ground out of. Convenience, never contract: a kit
|
|
12
|
+
// writing its own harbor may write these three again.
|
|
13
|
+
export { maker, entropy, learnPk } from '../ward/ground.js';
|
package/dist/harbor/memory.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { BeingClass } from '../being/types.ts';
|
|
2
|
-
import type
|
|
1
|
+
import type { BeingClass, BeingLike } from '../being/types.ts';
|
|
2
|
+
import { type Ground, type WardPointers } from '../ward/ground.ts';
|
|
3
3
|
export type Booted = WardPointers & {
|
|
4
4
|
pk: string;
|
|
5
5
|
};
|
|
@@ -16,7 +16,7 @@ export declare class MemoryHarbor {
|
|
|
16
16
|
cut: boolean;
|
|
17
17
|
readonly partitions: Map<string, Record<string, unknown>>;
|
|
18
18
|
readonly wards: Map<string, Booted>;
|
|
19
|
-
readonly objects: WeakMap<object,
|
|
19
|
+
readonly objects: WeakMap<object, BeingLike>;
|
|
20
20
|
route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined>;
|
|
21
21
|
link(harbor: MemoryHarbor): void;
|
|
22
22
|
boot(seed: string, Ward: WardFactory, classes: Record<string, BeingClass>): Promise<Booted>;
|
package/dist/harbor/memory.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maker, entropy, learnPk } from '../ward/ground.js';
|
|
1
2
|
import { hex } from '../ward/arithmetic.js';
|
|
2
3
|
export class MemoryHarbor {
|
|
3
4
|
doors = new Map(); // ward pk -> door
|
|
@@ -42,26 +43,19 @@ export class MemoryHarbor {
|
|
|
42
43
|
const ground = {
|
|
43
44
|
seed,
|
|
44
45
|
memory,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!C)
|
|
49
|
-
return null;
|
|
50
|
-
const obj = new C(stance);
|
|
51
|
-
this.objects.set(stance.cells, obj);
|
|
52
|
-
return obj;
|
|
53
|
-
},
|
|
46
|
+
// One objects map for the whole harbor, not one per ward: every ward
|
|
47
|
+
// here is in this process and a test reaches any being of any of them.
|
|
48
|
+
instantiate: maker(this.objects, classes),
|
|
54
49
|
// bytes cross, copied, never a reference. both ways.
|
|
55
50
|
carry: async (farPk, bytes) => {
|
|
56
51
|
this.wire.push(hex(bytes));
|
|
57
52
|
const back = await this.route(farPk, new Uint8Array(bytes));
|
|
58
53
|
return back === undefined ? undefined : new Uint8Array(back);
|
|
59
54
|
},
|
|
60
|
-
random:
|
|
55
|
+
random: entropy,
|
|
61
56
|
};
|
|
62
57
|
const w = await Ward(ground);
|
|
63
|
-
const
|
|
64
|
-
const booted = { ...w, pk: bp.notes.pk };
|
|
58
|
+
const booted = { ...w, pk: await learnPk(w) };
|
|
65
59
|
this.doors.set(booted.pk, booted.door);
|
|
66
60
|
this.wards.set(seed, booted);
|
|
67
61
|
return booted;
|
package/dist/harbor/reach.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// at the far end, a connection that would not open, a socket that is gone.
|
|
20
20
|
// A reach that sent the bytes and lost the line afterwards answers nothing
|
|
21
21
|
// at all, and the ward's own bound ends the ask.
|
|
22
|
-
import { hex, unhex } from '../ward/arithmetic.js';
|
|
22
|
+
import { concat, hex, unhex } from '../ward/arithmetic.js';
|
|
23
23
|
const never = () => new Promise(() => { }); // sent, and no word since: the ward's bound decides
|
|
24
24
|
// A request reach to one listener.
|
|
25
25
|
export function request(url) {
|
|
@@ -28,8 +28,14 @@ export function request(url) {
|
|
|
28
28
|
close: () => { },
|
|
29
29
|
carry: async (pk, bytes) => {
|
|
30
30
|
let res;
|
|
31
|
+
// A body of its own, not the caller's view: fetch reads it after this
|
|
32
|
+
// line returns, and a buffer somebody else may write is not a body. The
|
|
33
|
+
// copy is its own ArrayBuffer, which is what a body is; the bytes handed
|
|
34
|
+
// in are typed as backed by anything, shared memory included.
|
|
35
|
+
const body = new Uint8Array(bytes.length);
|
|
36
|
+
body.set(bytes);
|
|
31
37
|
try {
|
|
32
|
-
res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body
|
|
38
|
+
res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body });
|
|
33
39
|
}
|
|
34
40
|
catch {
|
|
35
41
|
return undefined; // the connection never opened: nothing was delivered
|
|
@@ -137,7 +143,7 @@ export class Socket {
|
|
|
137
143
|
head[0] = back === undefined ? NONE : REPLY;
|
|
138
144
|
new DataView(head.buffer).setUint32(1, id);
|
|
139
145
|
if (line.readyState === OPEN)
|
|
140
|
-
line.send(back === undefined ? head : concat(head, back));
|
|
146
|
+
line.send(back === undefined ? head : concat([head, back]));
|
|
141
147
|
});
|
|
142
148
|
return;
|
|
143
149
|
}
|
|
@@ -176,16 +182,10 @@ export class Socket {
|
|
|
176
182
|
if (this.pending.size >= PENDING)
|
|
177
183
|
this.pending.delete(this.pending.keys().next().value); // the oldest, which the ward's bound ended long ago
|
|
178
184
|
this.pending.set(id, ok);
|
|
179
|
-
this.line.send(concat(head, bytes));
|
|
185
|
+
this.line.send(concat([head, bytes]));
|
|
180
186
|
});
|
|
181
187
|
}
|
|
182
188
|
close() {
|
|
183
189
|
this.line.close();
|
|
184
190
|
}
|
|
185
191
|
}
|
|
186
|
-
function concat(a, b) {
|
|
187
|
-
const out = new Uint8Array(a.length + b.length);
|
|
188
|
-
out.set(a, 0);
|
|
189
|
-
out.set(b, a.length);
|
|
190
|
-
return out;
|
|
191
|
-
}
|
package/dist/ward/allowance.js
CHANGED
|
@@ -33,8 +33,13 @@ export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
|
|
|
33
33
|
};
|
|
34
34
|
return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
|
|
35
35
|
}
|
|
36
|
-
// Whether a budget has anything left to spend
|
|
37
|
-
|
|
36
|
+
// Whether a budget has anything left to spend, and the one reader of it: the
|
|
37
|
+
// door calls this on arrival and nothing else asks the question anywhere. A
|
|
38
|
+
// time that is not a whole number above zero is not a budget that ran out, it
|
|
39
|
+
// is no budget at all, and the door refuses both the same way, since a peer
|
|
40
|
+
// learns nothing from the difference. `allow` never makes one, so only bytes
|
|
41
|
+
// off the road ever carry one.
|
|
42
|
+
export const spent = (a) => !(Number.isSafeInteger(a.time) && a.time > 0);
|
|
38
43
|
// What a wait that ran out comes back as. Its own value, held by nobody
|
|
39
44
|
// outside the ward, so no answer from any door can be mistaken for it.
|
|
40
45
|
export const LATE = Symbol('late');
|
|
@@ -17,6 +17,7 @@ export declare function sealingPair(seed: Uint8Array): Promise<Pair>;
|
|
|
17
17
|
export declare function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array>;
|
|
18
18
|
export declare function verify(message: Uint8Array, signature: Uint8Array, pk: Uint8Array): Promise<boolean>;
|
|
19
19
|
export declare function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uint8Array>;
|
|
20
|
+
export declare function derive(secret: Uint8Array, label: Uint8Array, bytes: number): Promise<Uint8Array>;
|
|
20
21
|
export declare function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
|
|
21
22
|
export declare function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
|
|
22
23
|
export declare function box(inside: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{
|
package/dist/ward/arithmetic.js
CHANGED
|
@@ -23,7 +23,7 @@ export const SIGNATURE = 64;
|
|
|
23
23
|
export const NONCE = 12;
|
|
24
24
|
export const TAG = 16;
|
|
25
25
|
const SEAL_INFO = new TextEncoder().encode('quo-seal');
|
|
26
|
-
const
|
|
26
|
+
const SALT = new Uint8Array(0);
|
|
27
27
|
// A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
|
|
28
28
|
const ED_SECRET = unhex('302e020100300506032b657004220420');
|
|
29
29
|
const X_SECRET = unhex('302e020100300506032b656e04220420');
|
|
@@ -132,11 +132,19 @@ export async function agree(secret, peerPk) {
|
|
|
132
132
|
throw new Error('dead agreement'); // a padlock that was not a real key
|
|
133
133
|
return shared;
|
|
134
134
|
}
|
|
135
|
+
// HKDF-SHA-256: bytes from a secret, under a label that says what they are
|
|
136
|
+
// for. No salt, because the secret handed in is already a secret of full
|
|
137
|
+
// strength and the label is what keeps one use apart from another. Every
|
|
138
|
+
// label in this kit is spelled where it is used, never here: this is
|
|
139
|
+
// arithmetic and a label is a decision.
|
|
140
|
+
export async function derive(secret, label, bytes) {
|
|
141
|
+
const material = await subtle().importKey('raw', secret, 'HKDF', false, ['deriveBits']);
|
|
142
|
+
return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: label }, material, bytes * 8));
|
|
143
|
+
}
|
|
135
144
|
// One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
|
|
136
145
|
// no randomness of its own: the key it pairs with is fresh on every message.
|
|
137
146
|
async function cipherKey(shared, use) {
|
|
138
|
-
const
|
|
139
|
-
const out = new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SEAL_SALT, info: SEAL_INFO }, material, (KEY + NONCE) * 8));
|
|
147
|
+
const out = await derive(shared, SEAL_INFO, KEY + NONCE);
|
|
140
148
|
return { key: await subtle().importKey('raw', out.subarray(0, KEY), 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
|
|
141
149
|
}
|
|
142
150
|
// The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
|
package/dist/ward/door.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Asker, BeingLike, JsonObject } from '../being/types.ts';
|
|
1
|
+
import type { Asker, BeingLike, JsonObject, OccupantRecord } from '../being/types.ts';
|
|
2
2
|
import type { Heirs } from './heirs.ts';
|
|
3
3
|
import { type ReplyPayload, type WardKey } from './seal.ts';
|
|
4
4
|
export type Door = {
|
|
5
5
|
key: string;
|
|
6
6
|
being: BeingLike;
|
|
7
7
|
cells: {
|
|
8
|
-
occupants: Record<string,
|
|
8
|
+
occupants: Record<string, OccupantRecord>;
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
11
|
export type Judged = {
|
package/dist/ward/door.js
CHANGED
|
@@ -66,9 +66,10 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
66
66
|
return null; // D1. it did not open. there is nobody to answer.
|
|
67
67
|
const { to, payload, ephemeralPk } = a;
|
|
68
68
|
const refuse = () => ({ reply: SILENCE, ephemeralPk, heard: false });
|
|
69
|
-
// D2
|
|
70
|
-
//
|
|
71
|
-
//
|
|
69
|
+
// D2. The allowance is read here, before anything is done under it, and
|
|
70
|
+
// here only: a time that is not a whole number above zero is malformed and
|
|
71
|
+
// is refused as one. Hops at zero is refused beside it, so that a relay
|
|
72
|
+
// chain invented later meets doors that already stop it. Nothing sets hops.
|
|
72
73
|
if (spent({ time: payload.time }) || payload.hops === 0)
|
|
73
74
|
return refuse();
|
|
74
75
|
const args = payload.args ?? {};
|
|
@@ -80,7 +81,10 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
80
81
|
const pub = pk !== null ? doors.get(pk) : undefined;
|
|
81
82
|
if (!pub || !(await verifyAsk(a, payload.by)))
|
|
82
83
|
return refuse();
|
|
83
|
-
|
|
84
|
+
// She answers, and the bit stays false: no key this door holds spoke.
|
|
85
|
+
// The public being is the one place a stranger is answered by design,
|
|
86
|
+
// so it is the one place a harbor must still be able to rate her.
|
|
87
|
+
return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: false };
|
|
84
88
|
}
|
|
85
89
|
const h = heirs.admits(to, payload.by);
|
|
86
90
|
if (!h) {
|
|
@@ -124,8 +128,13 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
124
128
|
// door never throws: that reply is noise, a plain silence sealed to a key
|
|
125
129
|
// nobody holds, and the same is written for anything else the seal will
|
|
126
130
|
// not take.
|
|
131
|
+
// `judge` reads rows it does not check: open() reads the partition's shape
|
|
132
|
+
// at birth, so a row it acts on is the shape it expects. That is the first
|
|
133
|
+
// line and this is the second, because "the door never throws" is a promise
|
|
134
|
+
// to the harbor, which has nobody to hand a rejection to and would count it
|
|
135
|
+
// as no answer at all. Anything unforeseen is the silence a stranger hears.
|
|
127
136
|
return async function door(bytes) {
|
|
128
|
-
const out = await judge(bytes);
|
|
137
|
+
const out = await judge(bytes).catch(() => null);
|
|
129
138
|
const reply = out?.reply ?? SILENCE;
|
|
130
139
|
const heard = out?.heard ?? false;
|
|
131
140
|
try {
|
package/dist/ward/ground.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Stance, BeingLike } from '../being/types.ts';
|
|
1
|
+
import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
|
|
2
2
|
export type Ground = {
|
|
3
3
|
seed: string | Uint8Array;
|
|
4
4
|
memory: Record<string, unknown>;
|
|
@@ -14,3 +14,6 @@ export type WardPointers = {
|
|
|
14
14
|
}>;
|
|
15
15
|
ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
16
16
|
};
|
|
17
|
+
export declare const maker: (objects: WeakMap<object, BeingLike>, ...registries: (Record<string, BeingClass> | undefined)[]) => Ground['instantiate'];
|
|
18
|
+
export declare const entropy: (n: number) => Uint8Array;
|
|
19
|
+
export declare function learnPk(w: WardPointers): Promise<string>;
|
package/dist/ward/ground.js
CHANGED
|
@@ -1 +1,38 @@
|
|
|
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
|
|
3
|
+
// themselves apart: what a memory harbor and a real one differ on is the
|
|
4
|
+
// route and the store, and nothing here. A second kit writes its own harbor
|
|
5
|
+
// and may write these again; they are convenience, never contract.
|
|
6
|
+
// The code half of a ground, and the map back to what it made. A class is
|
|
7
|
+
// found by own key only, since `constructor` is a name Object lends every
|
|
8
|
+
// registry, and the first registry holding the name wins, so a ward's own
|
|
9
|
+
// classes stand in front of the harbor's. The object is remembered by the
|
|
10
|
+
// cells the ward handed in, which is how a side reaches a being it made and
|
|
11
|
+
// how that hand follows her out when she is unbooted.
|
|
12
|
+
export const maker = (objects, ...registries) => (className, stance) => {
|
|
13
|
+
const r = registries.find((reg) => reg !== undefined && Object.hasOwn(reg, className));
|
|
14
|
+
const C = r?.[className];
|
|
15
|
+
if (!C)
|
|
16
|
+
return null;
|
|
17
|
+
const obj = new C(stance);
|
|
18
|
+
objects.set(stance.cells, obj);
|
|
19
|
+
return obj;
|
|
20
|
+
};
|
|
21
|
+
// Entropy, from the one place every terrain that runs Quo has it. Every key a
|
|
22
|
+
// ward mints is drawn from this, so a harbor that wants another source hands
|
|
23
|
+
// its own and nothing here has to know.
|
|
24
|
+
export const entropy = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
25
|
+
// A ward's pk, learned the way anyone learns anything: by asking. The empty
|
|
26
|
+
// ask on the ask pointer is the ward's own describe and its notes carry the
|
|
27
|
+
// pk. A harbor has no other way to it and wants none: the ward mints it from
|
|
28
|
+
// the seed, and a harbor that read it off the seed itself would be a second
|
|
29
|
+
// derivation to keep in step with the first. A ward that answers anything
|
|
30
|
+
// else is not one this harbor can route to, and says so here rather than
|
|
31
|
+
// leaving an undefined pk in a directory.
|
|
32
|
+
export async function learnPk(w) {
|
|
33
|
+
const notes = (await w.ask());
|
|
34
|
+
const pk = notes?.notes?.pk;
|
|
35
|
+
if (typeof pk !== 'string')
|
|
36
|
+
throw new Error('the ward did not say its pk');
|
|
37
|
+
return pk;
|
|
38
|
+
}
|
package/dist/ward/heirs.d.ts
CHANGED
|
@@ -6,9 +6,8 @@ export declare class Heirs {
|
|
|
6
6
|
open(heir: string, being: string, id: string): void;
|
|
7
7
|
close(heir: string): void;
|
|
8
8
|
gone(heir: string, by: string): boolean;
|
|
9
|
-
get(heir: string): Heir | undefined;
|
|
10
9
|
admits(heir: string, by: string): Heir | null;
|
|
11
10
|
spend(h: Heir, seq: number): boolean;
|
|
12
11
|
honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord;
|
|
13
|
-
settle(h: Heir, by: string, next: string | null):
|
|
12
|
+
settle(h: Heir, by: string, next: string | null): void;
|
|
14
13
|
}
|
package/dist/ward/heirs.js
CHANGED
|
@@ -33,9 +33,6 @@ export class Heirs {
|
|
|
33
33
|
const g = this.#p.gone[heir];
|
|
34
34
|
return !!g && (by === g.current || by === g.announced);
|
|
35
35
|
}
|
|
36
|
-
get(heir) {
|
|
37
|
-
return this.#p.heirs[heir];
|
|
38
|
-
}
|
|
39
36
|
// May `by` speak for this heir? Returns the record if so, null if not.
|
|
40
37
|
// Does not write: the caller verifies the signature first, then settles.
|
|
41
38
|
admits(heir, by) {
|
|
@@ -79,31 +76,33 @@ export class Heirs {
|
|
|
79
76
|
// neither: a call that binds nothing must not burn a number on its way to
|
|
80
77
|
// being refused, or a stranger who cannot be heard would still leave a mark
|
|
81
78
|
// behind her. Every write below this line is one that is going to hold.
|
|
79
|
+
// This is where a knock is judged and where `unannounced` is said. settle
|
|
80
|
+
// below is the writing alone, and it judges nothing: one owner for the
|
|
81
|
+
// question, one for the answer.
|
|
82
82
|
honour(h, by, next, seq) {
|
|
83
83
|
if (h.fresh && (next === null || next === h.current))
|
|
84
84
|
return 'unannounced'; // a knock without a key of her own binds nothing, and the heir is not a key of her own
|
|
85
85
|
if (!this.spend(h, seq))
|
|
86
86
|
return 'repeated';
|
|
87
|
-
|
|
88
|
-
this.#wrote(); // the number
|
|
89
|
-
return
|
|
87
|
+
this.settle(h, by, next);
|
|
88
|
+
this.#wrote(); // the number and the keys moved together
|
|
89
|
+
return true;
|
|
90
90
|
}
|
|
91
|
-
// The signature checked out. Settle the keys: a fresh
|
|
92
|
-
// to what it announced
|
|
93
|
-
// that the heir dies as it speaks;
|
|
94
|
-
// announcement; an announced key becomes current.
|
|
91
|
+
// The signature checked out and honour has judged. Settle the keys: a fresh
|
|
92
|
+
// heir rotates at once to what it announced, which honour has already held
|
|
93
|
+
// to being something other than itself, so that the heir dies as it speaks;
|
|
94
|
+
// a current key replaces its announcement; an announced key becomes current.
|
|
95
95
|
settle(h, by, next) {
|
|
96
96
|
if (h.fresh) {
|
|
97
|
-
if (next === null
|
|
98
|
-
return
|
|
97
|
+
if (next === null)
|
|
98
|
+
return; // honour judged this. the compiler has not read it, so the narrowing stays.
|
|
99
99
|
h.current = next;
|
|
100
100
|
h.announced = null;
|
|
101
101
|
h.fresh = false;
|
|
102
|
-
return
|
|
102
|
+
return;
|
|
103
103
|
}
|
|
104
104
|
if (by === h.announced)
|
|
105
105
|
h.current = by;
|
|
106
106
|
h.announced = next;
|
|
107
|
-
return true;
|
|
108
107
|
}
|
|
109
108
|
}
|