@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/src/harbor/core.ts
CHANGED
|
@@ -17,16 +17,31 @@
|
|
|
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.ts';
|
|
20
|
-
import type
|
|
20
|
+
import { maker, entropy as random, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
|
|
21
21
|
import type { BeingClass, BeingLike } from '../being/types.ts';
|
|
22
22
|
import { silence } from '../being/silence.ts';
|
|
23
23
|
import { request, type Reach } from './reach.ts';
|
|
24
|
-
import { openReply, wardSignPk } from '../ward/seal.ts';
|
|
24
|
+
import { isWardPk, openReply, wardSignPk } from '../ward/seal.ts';
|
|
25
25
|
import { concat, sealingPair, KEY } from '../ward/arithmetic.ts';
|
|
26
26
|
import { within, LATE } from '../ward/allowance.ts';
|
|
27
27
|
import type { Kept, Store, WardRecord } from './store.ts';
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// What a harbor hands its own device for one ward it serves. The two
|
|
30
|
+
// pointers, and beside them what the device needs to be a device: the pk it
|
|
31
|
+
// routes by, the name it is kept under, the record, a save the caller may
|
|
32
|
+
// wait for.
|
|
33
|
+
//
|
|
34
|
+
// And the beings the harbor made. `instantiate` is in the ground, so the
|
|
35
|
+
// harbor is the one that constructs every being of every ward it serves: it
|
|
36
|
+
// holds those objects because it made them, and `being` and `keys` say what
|
|
37
|
+
// it made. This is not a third path to a being. It is the device's own code
|
|
38
|
+
// reaching its own object in its own process, the same reach a being has on
|
|
39
|
+
// one she booted herself, and nothing of it crosses an edge. What a harbor
|
|
40
|
+
// never does is read the partition to decide anything, and the partition is
|
|
41
|
+
// not here: it holds every seed the ward has, and a side that wants to know
|
|
42
|
+
// which beings there are or which one is public asks the ward, which is
|
|
43
|
+
// what the ask pointer is for.
|
|
44
|
+
export type Hosted = WardPointers & { pk: string; name: string; record: WardRecord; being(key: string): BeingLike | undefined; keys(): string[]; save(): Promise<void> };
|
|
30
45
|
|
|
31
46
|
// One ward's saves, in a line: the ward says it wrote, the harbor writes the
|
|
32
47
|
// partition once the line is free, and never twice at once, so an older
|
|
@@ -42,6 +57,9 @@ export type Bound = { reach: Reach; held: boolean };
|
|
|
42
57
|
export type Loader = (record: WardRecord) => Promise<Record<string, BeingClass>>;
|
|
43
58
|
|
|
44
59
|
export const DEFAULT_CODE = 'classes/index.ts';
|
|
60
|
+
// The device's entropy, spelled once. A harbor draws it for a ward's seed at
|
|
61
|
+
// birth, for the lid of every probe it sends, and it hands the same function
|
|
62
|
+
// to every ward it boots as the ground's `random`.
|
|
45
63
|
// How long a probe waits for the door behind a claim. A claim that answers
|
|
46
64
|
// nothing in this time is not bound; the next announce is another chance.
|
|
47
65
|
const PROBE = 10_000;
|
|
@@ -104,6 +122,18 @@ export class Harbor {
|
|
|
104
122
|
for (const [pk, url] of Object.entries(await this.store.hints())) this.hint(pk, url);
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
// The object a ward writes into, by the name it is kept under: the
|
|
126
|
+
// partition the harbor was handed at boot and keeps so it can save it.
|
|
127
|
+
// Nothing in the harbor reads it, and it is not on `Hosted`, because a
|
|
128
|
+
// side that holds a ward would then hold every seed in it and reach any
|
|
129
|
+
// being past every door. It is here for one reader, the conformance
|
|
130
|
+
// suite, whose census is ward state read straight and whose forgeries are
|
|
131
|
+
// ward state written straight, the way `MemoryHarbor.partitions` is. A
|
|
132
|
+
// side asks the ward.
|
|
133
|
+
partitionOf(name: string): Record<string, unknown> | undefined {
|
|
134
|
+
return this.#saving.get(name)?.memory;
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
// ---- saving
|
|
108
138
|
|
|
109
139
|
// The ward wrote. Its partition is saved once the line is free, and once
|
|
@@ -191,9 +221,8 @@ export class Harbor {
|
|
|
191
221
|
// the one it sealed itself, and it reads nothing from it but that it is
|
|
192
222
|
// the silence a door owes such a box, signed by the claimed key.
|
|
193
223
|
async prove(pk: string, reach: Reach): Promise<boolean> {
|
|
194
|
-
if (
|
|
224
|
+
if (!isWardPk(pk)) return false;
|
|
195
225
|
try {
|
|
196
|
-
const random = (n: number) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
197
226
|
const lid = await sealingPair(random(KEY));
|
|
198
227
|
const back = await within(PROBE, reach.carry(pk, concat([lid.pk, random(KEY)])));
|
|
199
228
|
if (back === LATE || back === undefined) return false;
|
|
@@ -222,8 +251,18 @@ export class Harbor {
|
|
|
222
251
|
for (const [pk, b] of this.reaches) if (b.reach === reach && !pks.includes(pk)) this.reaches.delete(pk);
|
|
223
252
|
const claimed = [...new Set(pks)].slice(0, ANNOUNCE);
|
|
224
253
|
const prove = async (pk: string) => {
|
|
225
|
-
if (this.doors.has(pk)
|
|
254
|
+
if (this.doors.has(pk)) return;
|
|
255
|
+
// A line already held for this pk is not displaced by another line that
|
|
256
|
+
// proves the same pk. A proof says only that the ward is reachable that
|
|
257
|
+
// way, and a relay that forwards to the real ward proves as well as the
|
|
258
|
+
// ward's own line does: taking the binding would add a hop the relay
|
|
259
|
+
// chooses, and can drop. The first held line keeps it until it closes,
|
|
260
|
+
// which unbind says.
|
|
261
|
+
const standing = this.reaches.get(pk);
|
|
262
|
+
if (standing?.held && standing.reach !== reach) return;
|
|
263
|
+
if (!(await this.prove(pk, reach))) return;
|
|
226
264
|
if (announces.get(reach) !== said) return; // a newer announce has spoken since
|
|
265
|
+
if (this.reaches.get(pk)?.held && this.reaches.get(pk)!.reach !== reach) return; // one took it while this was proving
|
|
227
266
|
this.reaches.set(pk, { reach, held });
|
|
228
267
|
};
|
|
229
268
|
for (let i = 0; i < claimed.length; i += PROVING) await Promise.all(claimed.slice(i, i + PROVING).map(prove));
|
|
@@ -236,7 +275,7 @@ export class Harbor {
|
|
|
236
275
|
|
|
237
276
|
// Mint a seed and boot an empty ward under a name: a world born here.
|
|
238
277
|
async create(name: string, user = '', code = DEFAULT_CODE): Promise<Hosted> {
|
|
239
|
-
const kept: Kept = { seed:
|
|
278
|
+
const kept: Kept = { seed: random(KEY), partition: {}, record: { pk: '', code, user } };
|
|
240
279
|
await this.store.put(name, kept);
|
|
241
280
|
const hosted = await this.host(name, kept);
|
|
242
281
|
await this.store.record(name, hosted.record);
|
|
@@ -251,8 +290,12 @@ export class Harbor {
|
|
|
251
290
|
const h = this.wards.get(name);
|
|
252
291
|
if (h) {
|
|
253
292
|
await h.save();
|
|
293
|
+
// Gone for whoever still holds the old pointers, whose save closure has
|
|
294
|
+
// this row in hand, and out of the map, which now says what this harbor
|
|
295
|
+
// serves and nothing else.
|
|
254
296
|
const s = this.#saving.get(name);
|
|
255
297
|
if (s) s.gone = true;
|
|
298
|
+
this.#saving.delete(name);
|
|
256
299
|
this.wards.delete(name);
|
|
257
300
|
this.doors.delete(h.pk);
|
|
258
301
|
this.#announce(); // this harbor no longer claims it: a listener unbinds a pk an announce stops naming
|
|
@@ -260,10 +303,21 @@ export class Harbor {
|
|
|
260
303
|
return this.store.take(name);
|
|
261
304
|
}
|
|
262
305
|
|
|
263
|
-
// Put what another harbor dropped, and boot it: same seed, same pk.
|
|
306
|
+
// Put what another harbor dropped, and boot it: same seed, same pk. A ward
|
|
307
|
+
// that will not boot is not adopted. This is the one path where a partition
|
|
308
|
+
// written somewhere else arrives, so it is the one place the ward's own
|
|
309
|
+
// shape check is met, and the name is refused for as long as it is kept:
|
|
310
|
+
// a partition put and left would hold the name against every later try,
|
|
311
|
+
// with a copy of it in a store that can never serve it. It goes back out,
|
|
312
|
+
// and the only copy is the one the caller is still holding.
|
|
264
313
|
async adopt(name: string, kept: Kept): Promise<Hosted> {
|
|
265
314
|
await this.store.put(name, kept);
|
|
266
|
-
|
|
315
|
+
try {
|
|
316
|
+
return await this.host(name, kept);
|
|
317
|
+
} catch (e) {
|
|
318
|
+
await this.store.take(name);
|
|
319
|
+
throw e;
|
|
320
|
+
}
|
|
267
321
|
}
|
|
268
322
|
|
|
269
323
|
// A ward from what the store keeps: the ground, once, and two pointers.
|
|
@@ -274,33 +328,47 @@ export class Harbor {
|
|
|
274
328
|
const ground: Ground = {
|
|
275
329
|
seed,
|
|
276
330
|
memory,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (!C) return null;
|
|
281
|
-
const obj = new C(stance);
|
|
282
|
-
objects.set(stance.cells, obj);
|
|
283
|
-
return obj;
|
|
284
|
-
},
|
|
331
|
+
// This ward's own classes first, then the harbor's: a ward that names a
|
|
332
|
+
// class of its own is answered with hers.
|
|
333
|
+
instantiate: maker(objects, classes, this.classes),
|
|
285
334
|
carry: (pk, bytes) => this.carry(pk, new Uint8Array(bytes)),
|
|
286
|
-
random
|
|
335
|
+
random,
|
|
287
336
|
wrote: () => this.#wrote(name),
|
|
288
337
|
};
|
|
289
338
|
const saving: Saving = { memory, dirty: false, running: null, timer: undefined, gone: false };
|
|
290
339
|
this.#saving.set(name, saving);
|
|
291
|
-
|
|
340
|
+
// A ward that will not be born leaves nothing behind: a partition of a
|
|
341
|
+
// shape this kit cannot read throws here, and a name the harbor does not
|
|
342
|
+
// serve must not have a saving row saying it does. Learning the pk is
|
|
343
|
+
// inside this, because a ward that will not say its pk is a ward this
|
|
344
|
+
// harbor cannot route to and is no more born than one that threw.
|
|
345
|
+
let w: WardPointers, pk: string;
|
|
346
|
+
try {
|
|
347
|
+
w = await Ward(ground);
|
|
348
|
+
pk = await learnPk(w);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
saving.gone = true;
|
|
351
|
+
this.#saving.delete(name);
|
|
352
|
+
throw e;
|
|
353
|
+
}
|
|
292
354
|
// A save the caller may wait for: what the ward wrote so far is in the
|
|
293
355
|
// store when this resolves, or the fault is counted. After a call through
|
|
294
356
|
// a pointer the store is current, so a restart right after it finds
|
|
295
|
-
// everything; a refusal at the door wrote nothing and saves nothing
|
|
357
|
+
// everything; a refusal at the door wrote nothing and saves nothing, and
|
|
358
|
+
// neither does a read. A ward that wrote nothing has nothing to save, so
|
|
359
|
+
// asking it what it holds is free, which is what makes the ask pointer
|
|
360
|
+
// the way to find out rather than an expensive way.
|
|
296
361
|
const save = async () => {
|
|
297
362
|
if (saving.gone) return;
|
|
298
|
-
saving.dirty = true;
|
|
299
363
|
await this.#flush(name);
|
|
300
364
|
};
|
|
301
365
|
const door = async (bytes: Uint8Array) => {
|
|
302
366
|
const r = await w.door(bytes);
|
|
303
|
-
|
|
367
|
+
// The reply waits for the store, so a restart right after it finds
|
|
368
|
+
// what the arrival wrote. Whether a key this door holds spoke is not
|
|
369
|
+
// the test: a stranger at the public being is not heard and her being
|
|
370
|
+
// may still have written, and a refusal wrote nothing whoever made it.
|
|
371
|
+
await save();
|
|
304
372
|
return r;
|
|
305
373
|
};
|
|
306
374
|
const ask = async (method?: string, args?: Record<string, unknown>) => {
|
|
@@ -309,13 +377,18 @@ export class Harbor {
|
|
|
309
377
|
await save();
|
|
310
378
|
return out;
|
|
311
379
|
};
|
|
312
|
-
|
|
380
|
+
// The objects map is keyed by the cells the ward handed instantiate, so
|
|
381
|
+
// the way back to a being is her cells; the partition is read for that
|
|
382
|
+
// one lookup and for nothing else, and a key with no object is a being
|
|
383
|
+
// who is not here this run. `keys` is what the map holds, so a caller
|
|
384
|
+
// never needs the beings table to find out what to ask for.
|
|
313
385
|
const beings = memory.beings as Record<string, object> | undefined;
|
|
314
386
|
const being = (key: string) => {
|
|
315
|
-
const cells = beings
|
|
387
|
+
const cells = beings && Object.hasOwn(beings, key) ? beings[key] : undefined;
|
|
316
388
|
return cells ? objects.get(cells) : undefined;
|
|
317
389
|
};
|
|
318
|
-
const
|
|
390
|
+
const keys = () => Object.keys(beings ?? {}).filter((k) => being(k) !== undefined);
|
|
391
|
+
const hosted: Hosted = { door, ask, pk, name, record: { ...kept.record, pk }, being, keys, save };
|
|
319
392
|
this.wards.set(name, hosted);
|
|
320
393
|
this.doors.set(hosted.pk, door);
|
|
321
394
|
await save();
|
package/src/harbor/dial.ts
CHANGED
|
@@ -12,11 +12,22 @@ import type { Harbor } from './core.ts';
|
|
|
12
12
|
import { REFUSED, Socket, type Line } from './reach.ts';
|
|
13
13
|
|
|
14
14
|
// `wake` is the device saying it is back: a phone out of a pocket, a
|
|
15
|
-
// laptop's lid opened. A device that slept comes back with its timer
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
15
|
+
// laptop's lid opened. A device that slept comes back with its timer frozen
|
|
16
|
+
// and, often, its socket closed under it by the system without a word. So
|
|
17
|
+
// nothing in hand is trusted: whatever line there is, open or still opening,
|
|
18
|
+
// is put down and a new one is dialed now.
|
|
19
|
+
//
|
|
20
|
+
// Nothing here can tell a line the system dropped from a live one, and
|
|
21
|
+
// asking is worse than dialing. An announce written into a dead line is
|
|
22
|
+
// taken into the send buffer and the error comes when TCP gives up
|
|
23
|
+
// retransmitting, one to two minutes on a Mac and up to fifteen on Linux,
|
|
24
|
+
// and for all of it every ask to a pk bound behind that line hears `late`.
|
|
25
|
+
// A probe with a bound would want a round trip the wire does not have: four
|
|
26
|
+
// words, none of them a ping, and making one out of an ask would give a
|
|
27
|
+
// second kit's listener a meaning to reproduce. Dialing costs one handshake
|
|
28
|
+
// when the line was fine, and an ask made meanwhile waits on the new socket
|
|
29
|
+
// rather than being lost, because a socket is a fallback from the moment it
|
|
30
|
+
// is dialed.
|
|
20
31
|
export type Dialer = { url: string; socket: Socket | null; wake(): void; close(): void };
|
|
21
32
|
|
|
22
33
|
export function dial(harbor: Harbor, url: string): Dialer {
|
|
@@ -24,7 +35,8 @@ export function dial(harbor: Harbor, url: string): Dialer {
|
|
|
24
35
|
let stopped = false,
|
|
25
36
|
wait = 1000,
|
|
26
37
|
timer: ReturnType<typeof setTimeout> | undefined,
|
|
27
|
-
line: Line | undefined
|
|
38
|
+
line: Line | undefined, // the line in hand, open or still opening: close() closes it either way
|
|
39
|
+
held: { socket: Socket; shed: () => void } | undefined; // the one this dialer owns now, and how it gives back what the line took
|
|
28
40
|
const connect = () => {
|
|
29
41
|
timer = undefined; // no wait pending: this is the dial it was waiting for
|
|
30
42
|
if (stopped) return;
|
|
@@ -41,11 +53,14 @@ export function dial(harbor: Harbor, url: string): Dialer {
|
|
|
41
53
|
void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
|
|
42
54
|
},
|
|
43
55
|
(why) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
shed();
|
|
57
|
+
// A line this dialer already put down. Its close arrives whenever the
|
|
58
|
+
// system gets round to noticing, which on a socket that died under a
|
|
59
|
+
// sleeping device is minutes; by then the line dialed in its place is
|
|
60
|
+
// the one in hand, so this one gives back what it took and dials
|
|
61
|
+
// nothing. Only the line the dialer still holds dials again.
|
|
62
|
+
if (held?.socket !== s) return;
|
|
63
|
+
held = undefined;
|
|
49
64
|
// A suite this listener will not speak is not a line that dropped. It
|
|
50
65
|
// will not become speakable by asking again sooner, so the wait goes
|
|
51
66
|
// straight to the ceiling and stays there until it changes its mind.
|
|
@@ -54,6 +69,16 @@ export function dial(harbor: Harbor, url: string): Dialer {
|
|
|
54
69
|
wait = Math.min(wait * 2, 30000);
|
|
55
70
|
},
|
|
56
71
|
);
|
|
72
|
+
// Everything this line took from the harbor, given back. Said when it
|
|
73
|
+
// closes, and when the dialer puts it down without waiting for that.
|
|
74
|
+
const shed = () => {
|
|
75
|
+
harbor.announcers.delete(say);
|
|
76
|
+
harbor.unbind(s);
|
|
77
|
+
if (d.socket === s) d.socket = null;
|
|
78
|
+
const at = harbor.fallbacks.indexOf(s);
|
|
79
|
+
if (at !== -1) harbor.fallbacks.splice(at, 1); // this line only: another dialer's stays
|
|
80
|
+
};
|
|
81
|
+
held = { socket: s, shed };
|
|
57
82
|
harbor.fallbacks.push(s); // from the moment it is dialed: an ask made before the handshake waits on the socket, and is not unreached
|
|
58
83
|
// What this harbor holds, said when the line opens and again whenever it
|
|
59
84
|
// changes. A ward booted or adopted after the handshake is announced on
|
|
@@ -68,11 +93,13 @@ export function dial(harbor: Harbor, url: string): Dialer {
|
|
|
68
93
|
};
|
|
69
94
|
d.wake = () => {
|
|
70
95
|
if (stopped) return;
|
|
71
|
-
if (d.socket) return d.socket.announce([...harbor.doors.keys()]);
|
|
72
|
-
if (timer === undefined) return;
|
|
73
96
|
clearTimeout(timer);
|
|
74
97
|
timer = undefined;
|
|
75
|
-
wait = 1000;
|
|
98
|
+
wait = 1000; // the device is back, and the wait it froze belongs to the sleep, not to this line
|
|
99
|
+
const old = held;
|
|
100
|
+
held = undefined; // put down before the new one is dialed, so a fallback list never holds the dead line ahead of the live one
|
|
101
|
+
old?.shed();
|
|
102
|
+
old?.socket.close();
|
|
76
103
|
connect();
|
|
77
104
|
};
|
|
78
105
|
d.close = () => {
|
package/src/harbor/index.ts
CHANGED
|
@@ -9,3 +9,6 @@ export { dial, type Dialer } from './dial.ts';
|
|
|
9
9
|
export { MemoryStore, values, type Store, type Kept, type WardRecord } from './store.ts';
|
|
10
10
|
export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
|
|
11
11
|
export type { Ground, WardPointers } from '../ward/ground.ts';
|
|
12
|
+
// What a harbor builds a ground out of. Convenience, never contract: a kit
|
|
13
|
+
// writing its own harbor may write these three again.
|
|
14
|
+
export { maker, entropy, learnPk } from '../ward/ground.ts';
|
package/src/harbor/memory.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// keeping TWO pointers. Routes one ward pk to one door. Knows no being,
|
|
5
5
|
// reads no partition, opens no byte. Several memory harbors may be linked,
|
|
6
6
|
// which stands in for a wire, and cut, which stands in for weather.
|
|
7
|
-
import type { BeingClass,
|
|
8
|
-
import type
|
|
7
|
+
import type { BeingClass, BeingLike } from '../being/types.ts';
|
|
8
|
+
import { maker, entropy, learnPk, type Ground, type WardPointers } from '../ward/ground.ts';
|
|
9
9
|
import { hex } from '../ward/arithmetic.ts';
|
|
10
10
|
|
|
11
11
|
export type Booted = WardPointers & { pk: string };
|
|
@@ -20,7 +20,7 @@ export class MemoryHarbor {
|
|
|
20
20
|
cut = false;
|
|
21
21
|
readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
|
|
22
22
|
readonly wards = new Map<string, Booted>(); // seed -> pointers
|
|
23
|
-
readonly objects = new WeakMap<object,
|
|
23
|
+
readonly objects = new WeakMap<object, BeingLike>(); // cells -> being object. what instantiate constructed. a hand for tests, never the ward's; it follows the cells out when she is unbooted
|
|
24
24
|
|
|
25
25
|
// the directory: its own doors, else a peer it is linked to. Quo says nothing about how.
|
|
26
26
|
async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
@@ -54,25 +54,19 @@ export class MemoryHarbor {
|
|
|
54
54
|
const ground: Ground = {
|
|
55
55
|
seed,
|
|
56
56
|
memory,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!C) return null;
|
|
61
|
-
const obj = new C(stance);
|
|
62
|
-
this.objects.set(stance.cells, obj);
|
|
63
|
-
return obj;
|
|
64
|
-
},
|
|
57
|
+
// One objects map for the whole harbor, not one per ward: every ward
|
|
58
|
+
// here is in this process and a test reaches any being of any of them.
|
|
59
|
+
instantiate: maker(this.objects, classes),
|
|
65
60
|
// bytes cross, copied, never a reference. both ways.
|
|
66
61
|
carry: async (farPk, bytes) => {
|
|
67
62
|
this.wire.push(hex(bytes));
|
|
68
63
|
const back = await this.route(farPk, new Uint8Array(bytes));
|
|
69
64
|
return back === undefined ? undefined : new Uint8Array(back);
|
|
70
65
|
},
|
|
71
|
-
random:
|
|
66
|
+
random: entropy,
|
|
72
67
|
};
|
|
73
68
|
const w = await Ward(ground);
|
|
74
|
-
const
|
|
75
|
-
const booted: Booted = { ...w, pk: bp.notes.pk };
|
|
69
|
+
const booted: Booted = { ...w, pk: await learnPk(w) };
|
|
76
70
|
this.doors.set(booted.pk, booted.door);
|
|
77
71
|
this.wards.set(seed, booted);
|
|
78
72
|
return booted;
|
package/src/harbor/reach.ts
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.ts';
|
|
22
|
+
import { concat, hex, unhex } from '../ward/arithmetic.ts';
|
|
23
23
|
|
|
24
24
|
export type Carry = (pk: string, bytes: Uint8Array) => Promise<Uint8Array | undefined>;
|
|
25
25
|
export type Reach = { carry: Carry; close(): void };
|
|
@@ -33,8 +33,14 @@ export function request(url: string): Reach {
|
|
|
33
33
|
close: () => {},
|
|
34
34
|
carry: async (pk, bytes) => {
|
|
35
35
|
let res: Response;
|
|
36
|
+
// A body of its own, not the caller's view: fetch reads it after this
|
|
37
|
+
// line returns, and a buffer somebody else may write is not a body. The
|
|
38
|
+
// copy is its own ArrayBuffer, which is what a body is; the bytes handed
|
|
39
|
+
// in are typed as backed by anything, shared memory included.
|
|
40
|
+
const body = new Uint8Array(bytes.length);
|
|
41
|
+
body.set(bytes);
|
|
36
42
|
try {
|
|
37
|
-
res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body
|
|
43
|
+
res = await fetch(`${base}/${pk}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream', 'quo-suite': String(SUITE) }, body });
|
|
38
44
|
} catch {
|
|
39
45
|
return undefined; // the connection never opened: nothing was delivered
|
|
40
46
|
}
|
|
@@ -163,7 +169,7 @@ export class Socket implements Reach {
|
|
|
163
169
|
const head = new Uint8Array(5);
|
|
164
170
|
head[0] = back === undefined ? NONE : REPLY;
|
|
165
171
|
new DataView(head.buffer).setUint32(1, id);
|
|
166
|
-
if (line.readyState === OPEN) line.send(back === undefined ? head : concat(head, back));
|
|
172
|
+
if (line.readyState === OPEN) line.send(back === undefined ? head : concat([head, back]));
|
|
167
173
|
});
|
|
168
174
|
return;
|
|
169
175
|
}
|
|
@@ -199,7 +205,7 @@ export class Socket implements Reach {
|
|
|
199
205
|
return new Promise((ok) => {
|
|
200
206
|
if (this.pending.size >= PENDING) this.pending.delete(this.pending.keys().next().value!); // the oldest, which the ward's bound ended long ago
|
|
201
207
|
this.pending.set(id, ok);
|
|
202
|
-
this.line.send(concat(head, bytes));
|
|
208
|
+
this.line.send(concat([head, bytes]));
|
|
203
209
|
});
|
|
204
210
|
}
|
|
205
211
|
|
|
@@ -207,10 +213,3 @@ export class Socket implements Reach {
|
|
|
207
213
|
this.line.close();
|
|
208
214
|
}
|
|
209
215
|
}
|
|
210
|
-
|
|
211
|
-
function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
212
|
-
const out = new Uint8Array(a.length + b.length);
|
|
213
|
-
out.set(a, 0);
|
|
214
|
-
out.set(b, a.length);
|
|
215
|
-
return out;
|
|
216
|
-
}
|
package/src/ward/allowance.ts
CHANGED
|
@@ -43,8 +43,13 @@ export function allow(wanted: Wanted | undefined, ceiling: Allowance = CEILING,
|
|
|
43
43
|
return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Whether a budget has anything left to spend
|
|
47
|
-
|
|
46
|
+
// Whether a budget has anything left to spend, and the one reader of it: the
|
|
47
|
+
// door calls this on arrival and nothing else asks the question anywhere. A
|
|
48
|
+
// time that is not a whole number above zero is not a budget that ran out, it
|
|
49
|
+
// is no budget at all, and the door refuses both the same way, since a peer
|
|
50
|
+
// learns nothing from the difference. `allow` never makes one, so only bytes
|
|
51
|
+
// off the road ever carry one.
|
|
52
|
+
export const spent = (a: Allowance): boolean => !(Number.isSafeInteger(a.time) && a.time > 0);
|
|
48
53
|
|
|
49
54
|
// What a wait that ran out comes back as. Its own value, held by nobody
|
|
50
55
|
// outside the ward, so no answer from any door can be mistaken for it.
|
package/src/ward/arithmetic.ts
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
|
|
|
28
28
|
// A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
|
|
29
29
|
const ED_SECRET = unhex('302e020100300506032b657004220420');
|
|
@@ -135,22 +135,31 @@ export async function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uin
|
|
|
135
135
|
return shared;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// HKDF-SHA-256: bytes from a secret, under a label that says what they are
|
|
139
|
+
// for. No salt, because the secret handed in is already a secret of full
|
|
140
|
+
// strength and the label is what keeps one use apart from another. Every
|
|
141
|
+
// label in this kit is spelled where it is used, never here: this is
|
|
142
|
+
// arithmetic and a label is a decision.
|
|
143
|
+
export async function derive(secret: Uint8Array, label: Uint8Array, bytes: number): Promise<Uint8Array> {
|
|
144
|
+
const material = await subtle().importKey('raw', secret as BufferSource, 'HKDF', false, ['deriveBits']);
|
|
145
|
+
return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: label as BufferSource }, material, bytes * 8));
|
|
146
|
+
}
|
|
147
|
+
|
|
138
148
|
// One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
|
|
139
149
|
// no randomness of its own: the key it pairs with is fresh on every message.
|
|
140
150
|
async function cipherKey(shared: Uint8Array, use: KeyUsage) {
|
|
141
|
-
const
|
|
142
|
-
const out = new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SEAL_SALT, info: SEAL_INFO }, material, (KEY + NONCE) * 8));
|
|
151
|
+
const out = await derive(shared, SEAL_INFO, KEY + NONCE);
|
|
143
152
|
return { key: await subtle().importKey('raw', out.subarray(0, KEY) as BufferSource, 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
|
|
144
153
|
}
|
|
145
154
|
// The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
|
|
146
155
|
export async function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
|
|
147
156
|
const { key, nonce } = await cipherKey(shared, 'encrypt');
|
|
148
|
-
return new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, plaintext as BufferSource));
|
|
157
|
+
return new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: nonce as BufferSource, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, plaintext as BufferSource));
|
|
149
158
|
}
|
|
150
159
|
export async function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
|
|
151
160
|
if (ciphertext.length < TAG) throw new Error('short input');
|
|
152
161
|
const { key, nonce } = await cipherKey(shared, 'decrypt');
|
|
153
|
-
return new Uint8Array(await subtle().decrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, ciphertext as BufferSource));
|
|
162
|
+
return new Uint8Array(await subtle().decrypt({ name: 'AES-GCM', iv: nonce as BufferSource, additionalData: key32(aad, 'aad') as BufferSource, tagLength: TAG * 8 }, key, ciphertext as BufferSource));
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
// A box: an ephemeral X25519 pk outside, one ciphertext sealed to the
|
package/src/ward/door.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// asker rides back with the object.
|
|
10
10
|
import { isSilence, isWord } from '../being/silence.ts';
|
|
11
11
|
import { digest } from '../being/digest.ts';
|
|
12
|
-
import type { Asker, BeingLike, DoorWord, JsonObject } from '../being/types.ts';
|
|
12
|
+
import type { Asker, BeingLike, DoorWord, JsonObject, OccupantRecord } from '../being/types.ts';
|
|
13
13
|
import { at } from './partition.ts';
|
|
14
14
|
import type { Heirs } from './heirs.ts';
|
|
15
15
|
import { openAsk, sealReply, verifyAsk, type ReplyPayload, type WardKey } from './seal.ts';
|
|
@@ -17,7 +17,7 @@ import { spent } from './allowance.ts';
|
|
|
17
17
|
import { KEY, sealingPair } from './arithmetic.ts';
|
|
18
18
|
import { cellFault } from './cells.ts';
|
|
19
19
|
|
|
20
|
-
export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string,
|
|
20
|
+
export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, OccupantRecord> } };
|
|
21
21
|
export type Judged = { bytes: Uint8Array; heard: boolean };
|
|
22
22
|
const SILENCE: ReplyPayload = { silence: true };
|
|
23
23
|
const said = (quo: DoorWord): ReplyPayload => ({ quo });
|
|
@@ -69,9 +69,10 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
|
|
|
69
69
|
if (!a) return null; // D1. it did not open. there is nobody to answer.
|
|
70
70
|
const { to, payload, ephemeralPk } = a;
|
|
71
71
|
const refuse = (): Verdict => ({ reply: SILENCE, ephemeralPk, heard: false });
|
|
72
|
-
// D2
|
|
73
|
-
//
|
|
74
|
-
//
|
|
72
|
+
// D2. The allowance is read here, before anything is done under it, and
|
|
73
|
+
// here only: a time that is not a whole number above zero is malformed and
|
|
74
|
+
// is refused as one. Hops at zero is refused beside it, so that a relay
|
|
75
|
+
// chain invented later meets doors that already stop it. Nothing sets hops.
|
|
75
76
|
if (spent({ time: payload.time }) || payload.hops === 0) return refuse();
|
|
76
77
|
const args = payload.args ?? {};
|
|
77
78
|
if (to === null) {
|
|
@@ -81,7 +82,10 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
|
|
|
81
82
|
const pk = publicKey();
|
|
82
83
|
const pub = pk !== null ? doors.get(pk) : undefined;
|
|
83
84
|
if (!pub || !(await verifyAsk(a, payload.by))) return refuse();
|
|
84
|
-
|
|
85
|
+
// She answers, and the bit stays false: no key this door holds spoke.
|
|
86
|
+
// The public being is the one place a stranger is answered by design,
|
|
87
|
+
// so it is the one place a harbor must still be able to rate her.
|
|
88
|
+
return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: false };
|
|
85
89
|
}
|
|
86
90
|
const h = heirs.admits(to, payload.by);
|
|
87
91
|
if (!h) {
|
|
@@ -120,8 +124,13 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
|
|
|
120
124
|
// door never throws: that reply is noise, a plain silence sealed to a key
|
|
121
125
|
// nobody holds, and the same is written for anything else the seal will
|
|
122
126
|
// not take.
|
|
127
|
+
// `judge` reads rows it does not check: open() reads the partition's shape
|
|
128
|
+
// at birth, so a row it acts on is the shape it expects. That is the first
|
|
129
|
+
// line and this is the second, because "the door never throws" is a promise
|
|
130
|
+
// to the harbor, which has nobody to hand a rejection to and would count it
|
|
131
|
+
// as no answer at all. Anything unforeseen is the silence a stranger hears.
|
|
123
132
|
return async function door(bytes: Uint8Array): Promise<Judged> {
|
|
124
|
-
const out = await judge(bytes);
|
|
133
|
+
const out = await judge(bytes).catch(() => null);
|
|
125
134
|
const reply = out?.reply ?? SILENCE;
|
|
126
135
|
const heard = out?.heard ?? false;
|
|
127
136
|
try {
|
package/src/ward/ground.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The ground. The one object a harbor passes a ward at birth. Six things,
|
|
3
3
|
// never a seventh. Everything a runtime differs on arrives here, which is
|
|
4
4
|
// why the ward itself knows no runtime.
|
|
5
|
-
import type { Stance, BeingLike } from '../being/types.ts';
|
|
5
|
+
import type { Stance, BeingLike, BeingClass } from '../being/types.ts';
|
|
6
6
|
|
|
7
7
|
export type Ground = {
|
|
8
8
|
seed: string | Uint8Array; // the ward derives its pk from it and nothing else
|
|
@@ -44,3 +44,45 @@ export type WardPointers = {
|
|
|
44
44
|
door(bytes: Uint8Array): Promise<{ bytes: Uint8Array; heard: boolean }>;
|
|
45
45
|
ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
46
46
|
};
|
|
47
|
+
|
|
48
|
+
// ---- 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
|
|
50
|
+
// themselves apart: what a memory harbor and a real one differ on is the
|
|
51
|
+
// route and the store, and nothing here. A second kit writes its own harbor
|
|
52
|
+
// and may write these again; they are convenience, never contract.
|
|
53
|
+
|
|
54
|
+
// The code half of a ground, and the map back to what it made. A class is
|
|
55
|
+
// found by own key only, since `constructor` is a name Object lends every
|
|
56
|
+
// registry, and the first registry holding the name wins, so a ward's own
|
|
57
|
+
// classes stand in front of the harbor's. The object is remembered by the
|
|
58
|
+
// cells the ward handed in, which is how a side reaches a being it made and
|
|
59
|
+
// how that hand follows her out when she is unbooted.
|
|
60
|
+
export const maker =
|
|
61
|
+
(objects: WeakMap<object, BeingLike>, ...registries: (Record<string, BeingClass> | undefined)[]): Ground['instantiate'] =>
|
|
62
|
+
(className: string, stance: Stance) => {
|
|
63
|
+
const r = registries.find((reg) => reg !== undefined && Object.hasOwn(reg, className));
|
|
64
|
+
const C = r?.[className];
|
|
65
|
+
if (!C) return null;
|
|
66
|
+
const obj = new C(stance);
|
|
67
|
+
objects.set(stance.cells, obj);
|
|
68
|
+
return obj;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Entropy, from the one place every terrain that runs Quo has it. Every key a
|
|
72
|
+
// ward mints is drawn from this, so a harbor that wants another source hands
|
|
73
|
+
// its own and nothing here has to know.
|
|
74
|
+
export const entropy = (n: number): Uint8Array => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
75
|
+
|
|
76
|
+
// A ward's pk, learned the way anyone learns anything: by asking. The empty
|
|
77
|
+
// ask on the ask pointer is the ward's own describe and its notes carry the
|
|
78
|
+
// pk. A harbor has no other way to it and wants none: the ward mints it from
|
|
79
|
+
// the seed, and a harbor that read it off the seed itself would be a second
|
|
80
|
+
// derivation to keep in step with the first. A ward that answers anything
|
|
81
|
+
// else is not one this harbor can route to, and says so here rather than
|
|
82
|
+
// leaving an undefined pk in a directory.
|
|
83
|
+
export async function learnPk(w: WardPointers): Promise<string> {
|
|
84
|
+
const notes = (await w.ask()) as { notes?: { pk?: unknown } } | null;
|
|
85
|
+
const pk = notes?.notes?.pk;
|
|
86
|
+
if (typeof pk !== 'string') throw new Error('the ward did not say its pk');
|
|
87
|
+
return pk;
|
|
88
|
+
}
|