@quo-systems/quo 0.1.1 → 0.2.0
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 +185 -114
- package/dist/being/index.d.ts +1 -1
- package/dist/being/index.js +1 -1
- package/dist/being/silence.d.ts +6 -1
- package/dist/being/silence.js +25 -7
- package/dist/being/types.d.ts +8 -4
- package/dist/being/types.js +1 -1
- package/dist/conformance/beings.d.ts +1 -1
- package/dist/conformance/beings.js +5 -5
- package/dist/conformance/estate.js +12 -6
- package/dist/conformance/index.js +14 -12
- package/dist/harbor/core.d.ts +6 -1
- package/dist/harbor/core.js +11 -2
- package/dist/harbor/memory.d.ts +5 -1
- package/dist/harbor/memory.js +7 -2
- package/dist/ward/door.d.ts +6 -2
- package/dist/ward/door.js +55 -35
- package/dist/ward/ground.d.ts +4 -1
- package/dist/ward/heirs.d.ts +4 -2
- package/dist/ward/heirs.js +20 -3
- package/dist/ward/owner.js +6 -4
- package/dist/ward/partition.d.ts +6 -0
- package/dist/ward/partition.js +2 -0
- package/dist/ward/seal.d.ts +3 -1
- package/dist/ward/seal.js +6 -2
- package/dist/ward/stance.d.ts +2 -2
- package/dist/ward/stance.js +21 -12
- package/dist/ward/ward.js +7 -7
- package/package.json +19 -31
- package/src/being/index.ts +1 -1
- package/src/being/silence.ts +26 -8
- package/src/being/types.ts +13 -3
- package/src/conformance/beings.ts +5 -5
- package/src/conformance/estate.ts +11 -6
- package/src/conformance/index.ts +14 -12
- package/src/harbor/core.ts +11 -2
- package/src/harbor/memory.ts +6 -1
- package/src/ward/door.ts +54 -32
- package/src/ward/ground.ts +6 -2
- package/src/ward/heirs.ts +20 -5
- package/src/ward/owner.ts +4 -3
- package/src/ward/partition.ts +10 -0
- package/src/ward/seal.ts +8 -5
- package/src/ward/stance.ts +20 -14
- package/src/ward/ward.ts +9 -9
- package/vectors/framing.json +7 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The fixture beings. Fixed, so that the ward is what varies. A printer, a
|
|
3
3
|
// shop, a customer on the base class, and one raw being with no base at all.
|
|
4
4
|
import { Being } from '../being/being.js';
|
|
5
|
-
import { isSilence, isUnreached } from '../being/silence.js';
|
|
5
|
+
import { isSilence, isUnreached, isWord } from '../being/silence.js';
|
|
6
6
|
// A printer. Invites whom she is told to, prints for her occupants. Her
|
|
7
7
|
// blueprint lives in her cells so a test can change her shape.
|
|
8
8
|
export class Printer extends Being {
|
|
@@ -41,7 +41,7 @@ export class Shop extends Being {
|
|
|
41
41
|
}
|
|
42
42
|
async keepPrinter(invitation) {
|
|
43
43
|
const bp = await this.knock(invitation);
|
|
44
|
-
if (isSilence(bp) ||
|
|
44
|
+
if (isSilence(bp) || isWord(bp))
|
|
45
45
|
return false;
|
|
46
46
|
this.cells.printerId = await this.take('prn', invitation);
|
|
47
47
|
return true;
|
|
@@ -50,7 +50,7 @@ export class Shop extends Being {
|
|
|
50
50
|
const rec = this.occupant(asker);
|
|
51
51
|
if (rec?.notes.tier === 'gold' && invitation) {
|
|
52
52
|
const back = await this.knock(invitation, 'hi');
|
|
53
|
-
if (!isSilence(back) && !
|
|
53
|
+
if (!isSilence(back) && !isWord(back))
|
|
54
54
|
await this.take(`vip-${asker.id}`, invitation);
|
|
55
55
|
}
|
|
56
56
|
return { welcome: true };
|
|
@@ -65,7 +65,7 @@ export class Shop extends Being {
|
|
|
65
65
|
const out = await printer.ask('print', { doc: `receipt for ${item}` });
|
|
66
66
|
if (isUnreached(out))
|
|
67
67
|
return { error: 'printer unreachable', retry: true };
|
|
68
|
-
if (isSilence(out))
|
|
68
|
+
if (isSilence(out) || isWord(out))
|
|
69
69
|
return { error: 'printer refused' };
|
|
70
70
|
const st = this.cells.standings[id];
|
|
71
71
|
if (st.seen !== st.digest)
|
|
@@ -85,7 +85,7 @@ export class Customer extends Being {
|
|
|
85
85
|
async join(invitation) {
|
|
86
86
|
const mine = (await this.invite('shop-back'));
|
|
87
87
|
const out = await this.knock(invitation, 'hello', { invitation: mine });
|
|
88
|
-
if (isSilence(out) ||
|
|
88
|
+
if (isSilence(out) || isWord(out))
|
|
89
89
|
return out; // nothing was born
|
|
90
90
|
await this.take('shop', invitation); // now, and only now
|
|
91
91
|
return out;
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
// A model that kept one symmetric edge would be a wrong model, and would
|
|
34
34
|
// agree happily with a ward that had drifted.
|
|
35
35
|
import { assert } from './assert.js';
|
|
36
|
-
import { isSilence, isUnreached } from '../being/silence.js';
|
|
36
|
+
import { isSilence, isUnreached, isWord, wordOf } from '../being/silence.js';
|
|
37
37
|
import { Member } from './beings.js';
|
|
38
38
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
39
|
+
const said = (x, w) => isWord(x) && wordOf(x) === w;
|
|
39
40
|
// A pseudo-random source in the language alone: the estate must replay, on
|
|
40
41
|
// every terrain, from a number a failure can print.
|
|
41
42
|
const rolls = (seed) => () => {
|
|
@@ -136,10 +137,11 @@ export function estate(label, make, { canDown, test }) {
|
|
|
136
137
|
const back = (await partner.being.invite('acme'));
|
|
137
138
|
assert.deepEqual(await acme.being.knock(back, 'ping'), { pong: 'acme' });
|
|
138
139
|
assert.equal(await acme.being.take('them', back), 'them');
|
|
139
|
-
// 5. acme kicks her. her standing is still hers to hold
|
|
140
|
-
// told
|
|
140
|
+
// 5. acme kicks her. her standing is still hers to hold, and she is not
|
|
141
|
+
// told until she asks: then the door, under the key it bound for her,
|
|
142
|
+
// says removed, and only to her.
|
|
141
143
|
acme.being.occupants.remove('partner-1');
|
|
142
|
-
assert.ok(
|
|
144
|
+
assert.ok(said(await partner.being.standings['the-client'].ask('ping'), 'removed'));
|
|
143
145
|
assert.deepEqual(Object.keys(partner.cells.standings), ['the-client']); // she never learns
|
|
144
146
|
assert.deepEqual(await acme.being.standings.them.ask('ping'), { pong: 'acme' }); // the other arc never moved
|
|
145
147
|
// 6. and back, as somebody else: a second invitation is a second relation,
|
|
@@ -148,7 +150,7 @@ export function estate(label, make, { canDown, test }) {
|
|
|
148
150
|
assert.notEqual(second.heir, first.heir);
|
|
149
151
|
assert.deepEqual(await partner.being.knock(second, 'ping'), { pong: 'partner-2' });
|
|
150
152
|
assert.equal(await partner.being.take('the-client-again', second), 'the-client-again');
|
|
151
|
-
assert.ok(
|
|
153
|
+
assert.ok(said(await partner.being.standings['the-client'].ask('ping'), 'removed'));
|
|
152
154
|
ledger(w.census(), undefined, 'the story');
|
|
153
155
|
});
|
|
154
156
|
for (const seed of [1, 2, 3]) {
|
|
@@ -193,6 +195,10 @@ export function estate(label, make, { canDown, test }) {
|
|
|
193
195
|
s.guest = guest;
|
|
194
196
|
s.answered = true;
|
|
195
197
|
}
|
|
198
|
+
else if (!s.live && (s.guest === null || (s.guest === guest && (hold === undefined || hold.live)))) {
|
|
199
|
+
// kicked, and she speaks under a key the door bound for this seat: she hears why
|
|
200
|
+
assert.ok(said(back, 'removed'), `${guest} knocking kicked ${s.host}/${s.id} should hear removed`);
|
|
201
|
+
}
|
|
196
202
|
else
|
|
197
203
|
assert.ok(isSilence(back), `${guest} knocking ${s.host}/${s.id} should be silence`);
|
|
198
204
|
return `${guest} knocks ${s.host}/${s.id}`;
|
|
@@ -274,7 +280,7 @@ export function estate(label, make, { canDown, test }) {
|
|
|
274
280
|
if (h.seat.live)
|
|
275
281
|
assert.deepEqual(out, { pong: h.seat.id }, `step ${step}: ${h.owner}/${h.id} -> ${h.seat.host}/${h.seat.id}`);
|
|
276
282
|
else
|
|
277
|
-
assert.ok(
|
|
283
|
+
assert.ok(said(out, 'removed'), `step ${step}: ${h.owner}/${h.id} is kicked and should hear removed`);
|
|
278
284
|
}
|
|
279
285
|
};
|
|
280
286
|
// The trace is the seed's own account of itself: a red run prints the
|
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Every assertion here is the truth's. Nothing here knows how a ward is built.
|
|
21
21
|
import { assert } from './assert.js';
|
|
22
|
-
import { silence, isSilence, isUnreached } from '../being/silence.js';
|
|
22
|
+
import { silence, isSilence, isUnreached, isWord, wordOf } from '../being/silence.js';
|
|
23
23
|
import { digest } from '../being/digest.js';
|
|
24
24
|
import { Printer, Shop, Customer, Echo, Member } from './beings.js';
|
|
25
25
|
import { estate } from './estate.js';
|
|
26
26
|
export { Printer, Shop, Customer, Echo, Member };
|
|
27
27
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
28
|
+
// Her ward's word, as she receives it.
|
|
29
|
+
const said = (x, w) => isWord(x) && wordOf(x) === w;
|
|
28
30
|
export function conform(label, make, { canDown = true, test }) {
|
|
29
31
|
const t = (name, fn) => test(`[${label}] ${name}`, {}, fn);
|
|
30
32
|
const down = (name, fn) => test(`[${label}] ${name}`, { skip: !canDown && 'nothing to cut inside one ward' }, fn);
|
|
@@ -62,12 +64,12 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
62
64
|
assert.notEqual(i1.secret, i2.secret);
|
|
63
65
|
assert.equal(i1.ward, i2.ward);
|
|
64
66
|
});
|
|
65
|
-
t('remove before any knock: the heir is forgotten, a knock on it
|
|
67
|
+
t('remove before any knock: the heir is forgotten, and a knock on it by whoever holds it hears removed', async () => {
|
|
66
68
|
const { w, shop, alice } = await world();
|
|
67
69
|
const inv = await shop.being.invite();
|
|
68
70
|
shop.being.occupants.remove('cust1');
|
|
69
71
|
assert.equal(w.heir(inv), undefined);
|
|
70
|
-
assert.ok(
|
|
72
|
+
assert.ok(said(await alice.being.join(inv), 'removed'));
|
|
71
73
|
assert.deepEqual(alice.cells.standings, {});
|
|
72
74
|
});
|
|
73
75
|
// ---- consume, the knock
|
|
@@ -143,15 +145,15 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
143
145
|
assert.deepEqual(await alice.being.join(inv), { welcome: true }); // the same invitation still works
|
|
144
146
|
const inv2 = await shop.being.invite();
|
|
145
147
|
shop.being.occupants.remove('cust2'); // she changed her mind before the knock
|
|
146
|
-
assert.ok(
|
|
148
|
+
assert.ok(said(await alice.being.join(inv2), 'removed'));
|
|
147
149
|
assert.deepEqual(Object.keys(alice.cells.standings), ['shop']);
|
|
148
150
|
});
|
|
149
|
-
t('take without a knock, or after a
|
|
151
|
+
t('take without a knock, or after a knock that was not answered, births nothing', async () => {
|
|
150
152
|
const { shop, alice } = await world();
|
|
151
153
|
const inv = await shop.being.invite();
|
|
152
154
|
assert.equal(await alice.being.take('shop', inv), null);
|
|
153
155
|
shop.being.occupants.remove('cust1');
|
|
154
|
-
assert.ok(
|
|
156
|
+
assert.ok(said(await alice.being.knock(inv, 'hello'), 'removed'));
|
|
155
157
|
assert.equal(await alice.being.take('shop', inv), null);
|
|
156
158
|
assert.deepEqual(alice.cells.standings, {});
|
|
157
159
|
});
|
|
@@ -311,16 +313,16 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
311
313
|
printer.down = false;
|
|
312
314
|
assert.deepEqual(await alice.being.buy('hat'), { ok: true, receipt: 'receipt for hat' });
|
|
313
315
|
});
|
|
314
|
-
t('a removed occupant
|
|
316
|
+
t('a removed occupant hears removed under the key the door bound, her heir is forgotten; a dropped standing is gone', async () => {
|
|
315
317
|
const { w, alice, shop } = await world();
|
|
316
318
|
const inv = await shop.being.invite();
|
|
317
319
|
await alice.being.join(inv);
|
|
318
320
|
shop.being.occupants.remove('cust1');
|
|
319
321
|
assert.equal(w.heir(inv), undefined);
|
|
320
|
-
assert.ok(
|
|
322
|
+
assert.ok(said(await alice.being.buy('hat'), 'removed'));
|
|
321
323
|
alice.being.standings.remove('shop');
|
|
322
324
|
assert.equal(alice.being.standings.shop, undefined);
|
|
323
|
-
assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); //
|
|
325
|
+
assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); // her bound key went with the standing: a re-knock is as the heir, which is spent, a stranger
|
|
324
326
|
const again = await shop.being.invite(); // cust2. a new heir
|
|
325
327
|
assert.notEqual(again.heir, inv.heir);
|
|
326
328
|
assert.deepEqual(await alice.being.knock(again, 'hello'), { welcome: true });
|
|
@@ -333,7 +335,7 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
333
335
|
assert.deepEqual(await p, { ok: true, receipt: 'receipt for hat' });
|
|
334
336
|
assert.equal(shop.cells.sales.length, 1);
|
|
335
337
|
});
|
|
336
|
-
t('a being that throws is
|
|
338
|
+
t('a being that throws is the word threw to whom the door has bound; the ward stays up', async () => {
|
|
337
339
|
const w = await make();
|
|
338
340
|
class Bomb {
|
|
339
341
|
s;
|
|
@@ -349,7 +351,7 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
349
351
|
const x = await w.boot('X', Bomb);
|
|
350
352
|
const a = await w.boot('A', Customer);
|
|
351
353
|
const inv = (await x.being.s.occupants.invite('a'));
|
|
352
|
-
assert.ok(
|
|
354
|
+
assert.ok(said(await a.being.knock(inv, 'any'), 'threw'));
|
|
353
355
|
assert.deepEqual(await a.being.knock(inv), { asks: [], notes: {} });
|
|
354
356
|
});
|
|
355
357
|
t('null is an answer, silence is not', async () => {
|
|
@@ -407,7 +409,7 @@ export function conform(label, make, { canDown = true, test }) {
|
|
|
407
409
|
if (!w.oneWard)
|
|
408
410
|
assert.ok(isSilence(await a.being.knock(at(priv), 'time'))); // a ward with no public being: silence
|
|
409
411
|
assert.ok(isUnreached(await a.being.knock({ ward: 'a'.repeat(128) }, 'time'))); // no door anywhere: unreached
|
|
410
|
-
assert.ok(
|
|
412
|
+
assert.ok(said(await a.being.knock({ ward: 'not a pk' }, 'time'), 'invitation')); // not even an address
|
|
411
413
|
assert.equal(await a.being.take('clock', at(pub)), 'clock');
|
|
412
414
|
assert.deepEqual(await a.being.standings.clock.ask('time'), { time: 12, got: {} });
|
|
413
415
|
assert.equal(a.cells.standings.clock.seen, await digest((await a.being.standings.clock.ask())));
|
package/dist/harbor/core.d.ts
CHANGED
|
@@ -17,12 +17,17 @@ export type Bound = {
|
|
|
17
17
|
export type Loader = (record: WardRecord) => Promise<Record<string, BeingClass>>;
|
|
18
18
|
export declare const DEFAULT_CODE = "classes/index.ts";
|
|
19
19
|
export declare class Harbor {
|
|
20
|
+
#private;
|
|
20
21
|
readonly store: Store;
|
|
21
22
|
readonly loader: Loader;
|
|
22
23
|
readonly wards: Map<string, Hosted>;
|
|
23
|
-
readonly doors: Map<string, (bytes: Uint8Array) => Promise<
|
|
24
|
+
readonly doors: Map<string, (bytes: Uint8Array) => Promise<{
|
|
25
|
+
bytes: Uint8Array;
|
|
26
|
+
heard: boolean;
|
|
27
|
+
}>>;
|
|
24
28
|
readonly reaches: Map<string, Bound>;
|
|
25
29
|
readonly down: Set<string>;
|
|
30
|
+
readonly refused: Map<string, number>;
|
|
26
31
|
readonly classes: Record<string, BeingClass>;
|
|
27
32
|
readonly fallbacks: Reach[];
|
|
28
33
|
constructor(store: Store, loader?: Loader);
|
package/dist/harbor/core.js
CHANGED
|
@@ -32,6 +32,7 @@ export class Harbor {
|
|
|
32
32
|
doors = new Map(); // ward pk -> door
|
|
33
33
|
reaches = new Map(); // the directory: foreign ward pk -> reach
|
|
34
34
|
down = new Set(); // pks this harbor will not reach right now: weather, for a test
|
|
35
|
+
refused = new Map(); // own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
|
|
35
36
|
classes = {}; // classes handed in-process, beside the loader's
|
|
36
37
|
// The dialers' sockets, in the order they were dialed: where a pk nobody
|
|
37
38
|
// here knows is sent, because the listener a harbor dialed is a rendezvous
|
|
@@ -56,12 +57,20 @@ export class Harbor {
|
|
|
56
57
|
this.hint(pk, url);
|
|
57
58
|
}
|
|
58
59
|
// ---- the directory
|
|
60
|
+
// Bytes to one of this harbor's own doors, and the one bit the door says
|
|
61
|
+
// beside its reply, counted.
|
|
62
|
+
async #own(pk, door, bytes) {
|
|
63
|
+
const r = await door(bytes);
|
|
64
|
+
if (!r.heard)
|
|
65
|
+
this.refused.set(pk, (this.refused.get(pk) ?? 0) + 1);
|
|
66
|
+
return new Uint8Array(r.bytes);
|
|
67
|
+
}
|
|
59
68
|
async carry(pk, bytes) {
|
|
60
69
|
if (this.down.has(pk))
|
|
61
70
|
return undefined;
|
|
62
71
|
const door = this.doors.get(pk);
|
|
63
72
|
if (door)
|
|
64
|
-
return
|
|
73
|
+
return this.#own(pk, door, bytes);
|
|
65
74
|
const bound = this.reaches.get(pk);
|
|
66
75
|
if (bound)
|
|
67
76
|
return bound.reach.carry(pk, bytes);
|
|
@@ -77,7 +86,7 @@ export class Harbor {
|
|
|
77
86
|
return undefined;
|
|
78
87
|
const door = this.doors.get(pk);
|
|
79
88
|
if (door)
|
|
80
|
-
return
|
|
89
|
+
return this.#own(pk, door, bytes);
|
|
81
90
|
const bound = this.reaches.get(pk);
|
|
82
91
|
return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
|
|
83
92
|
}
|
package/dist/harbor/memory.d.ts
CHANGED
|
@@ -5,9 +5,13 @@ export type Booted = WardPointers & {
|
|
|
5
5
|
};
|
|
6
6
|
export type WardFactory = (ground: Ground) => Promise<WardPointers>;
|
|
7
7
|
export declare class MemoryHarbor {
|
|
8
|
-
readonly doors: Map<string, (bytes: Uint8Array) => Promise<
|
|
8
|
+
readonly doors: Map<string, (bytes: Uint8Array) => Promise<{
|
|
9
|
+
bytes: Uint8Array;
|
|
10
|
+
heard: boolean;
|
|
11
|
+
}>>;
|
|
9
12
|
readonly down: Set<string>;
|
|
10
13
|
readonly wire: string[];
|
|
14
|
+
readonly refused: Map<string, number>;
|
|
11
15
|
readonly peers: Set<MemoryHarbor>;
|
|
12
16
|
cut: boolean;
|
|
13
17
|
readonly partitions: Map<string, Record<string, unknown>>;
|
package/dist/harbor/memory.js
CHANGED
|
@@ -3,6 +3,7 @@ export class MemoryHarbor {
|
|
|
3
3
|
doors = new Map(); // ward pk -> door
|
|
4
4
|
down = new Set(); // ward pks this harbor cannot reach right now
|
|
5
5
|
wire = []; // every byte string that ever crossed, as hex. for inspection
|
|
6
|
+
refused = new Map(); // ward pk -> arrivals its door would not admit: strangers, counted
|
|
6
7
|
peers = new Set();
|
|
7
8
|
cut = false;
|
|
8
9
|
partitions = new Map(); // seed -> memory. the harbor keeps it and reads nothing
|
|
@@ -13,8 +14,12 @@ export class MemoryHarbor {
|
|
|
13
14
|
if (this.down.has(farPk))
|
|
14
15
|
return undefined;
|
|
15
16
|
const door = this.doors.get(farPk);
|
|
16
|
-
if (door)
|
|
17
|
-
|
|
17
|
+
if (door) {
|
|
18
|
+
const r = await door(bytes);
|
|
19
|
+
if (!r.heard)
|
|
20
|
+
this.refused.set(farPk, (this.refused.get(farPk) ?? 0) + 1);
|
|
21
|
+
return r.bytes;
|
|
22
|
+
}
|
|
18
23
|
const peer = [...this.peers].find((h) => h.doors.has(farPk) && !h.down.has(farPk));
|
|
19
24
|
if (!peer || this.cut)
|
|
20
25
|
return undefined; // nothing. the harbor has no word silence
|
package/dist/ward/door.d.ts
CHANGED
|
@@ -8,5 +8,9 @@ export type Door = {
|
|
|
8
8
|
occupants: Record<string, unknown>;
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
|
-
export
|
|
12
|
-
|
|
11
|
+
export type Judged = {
|
|
12
|
+
bytes: Uint8Array;
|
|
13
|
+
heard: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
|
|
16
|
+
export declare function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array): (bytes: Uint8Array) => Promise<Judged>;
|
package/dist/ward/door.js
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Judgment at the door. Sealed bytes in, sealed bytes out
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
2
|
+
// Judgment at the door. Sealed bytes in, sealed bytes out, and one bit for
|
|
3
|
+
// the harbor: whether a key this door holds spoke. A stranger, bytes the
|
|
4
|
+
// door cannot admit, meets one silence whatever the case, so that it learns
|
|
5
|
+
// nothing, not even which case it hit. A key the door has bound hears the
|
|
6
|
+
// ward's word for what happened, sealed to its own lid, because it has
|
|
7
|
+
// already proven who it is and a reason to it is an oracle to nobody. What
|
|
8
|
+
// passes is named and dispatched to her answer, and her digest for that
|
|
9
|
+
// asker rides back with the object.
|
|
10
|
+
import { isSilence, isWord } from '../being/silence.js';
|
|
7
11
|
import { digest } from '../being/digest.js';
|
|
8
12
|
import { at } from './partition.js';
|
|
9
13
|
import { openAsk, sealReply, verifyAsk } from './seal.js';
|
|
10
14
|
import { spent } from './allowance.js';
|
|
11
15
|
import { KEY, sealingPair } from './arithmetic.js';
|
|
12
16
|
const SILENCE = { silence: true };
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const said = (quo) => ({ quo });
|
|
18
|
+
// One arrival at one being, already named. Catches every throw. `bound` says
|
|
19
|
+
// whether the asker is a key this door holds: she hears `threw`; a stranger
|
|
20
|
+
// at the public being hears silence, because her insides are hers.
|
|
21
|
+
export async function arrive(door, asker, method, args, bound) {
|
|
22
|
+
const threw = bound ? said('threw') : SILENCE;
|
|
15
23
|
let out;
|
|
16
24
|
try {
|
|
17
25
|
out = await door.being.answer(asker, method, args);
|
|
18
26
|
}
|
|
19
27
|
catch {
|
|
20
|
-
return
|
|
28
|
+
return threw; // she threw where she was asked. there is no answer.
|
|
21
29
|
}
|
|
22
30
|
// Nothing at all is not an answer either: a method that forgot to return
|
|
23
31
|
// has said nothing, and nothing is silence, never a value that JSON drops
|
|
24
32
|
// on the way out and the far side reads back as a fourth word.
|
|
25
|
-
if (out === undefined || isSilence(out)
|
|
33
|
+
if (out === undefined || isSilence(out))
|
|
26
34
|
return SILENCE;
|
|
35
|
+
if (isWord(out))
|
|
36
|
+
return threw; // a word is the ward's to say, never hers
|
|
27
37
|
if (method === undefined)
|
|
28
38
|
return { object: out, seen: null };
|
|
29
39
|
// The digest rides along, it is not the answer. She has already answered:
|
|
@@ -33,7 +43,7 @@ export async function arrive(door, asker, method, args) {
|
|
|
33
43
|
async function seen(door, asker) {
|
|
34
44
|
try {
|
|
35
45
|
const bp = await door.being.answer(asker);
|
|
36
|
-
return isSilence(bp) ||
|
|
46
|
+
return isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
37
47
|
}
|
|
38
48
|
catch {
|
|
39
49
|
return null;
|
|
@@ -43,41 +53,50 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
43
53
|
const judge = async (bytes) => {
|
|
44
54
|
const a = await openAsk(bytes, key.padlock);
|
|
45
55
|
if (!a)
|
|
46
|
-
return null; // it did not open. there is nobody to answer.
|
|
56
|
+
return null; // D1. it did not open. there is nobody to answer.
|
|
47
57
|
const { to, payload, ephemeralPk } = a;
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
if (spent({ time: payload.time }))
|
|
53
|
-
return
|
|
54
|
-
// An ask with no doors left is one silence like a budget already gone.
|
|
55
|
-
// Nothing sets hops and nothing decrements it yet; this door refuses zero
|
|
56
|
-
// so that a relay chain invented later meets doors that already stop it.
|
|
57
|
-
if (payload.hops === 0)
|
|
58
|
-
return { reply: SILENCE, ephemeralPk };
|
|
58
|
+
const refuse = () => ({ reply: SILENCE, ephemeralPk, heard: false });
|
|
59
|
+
// D2, the rest of it. The allowance is read before anything is done under
|
|
60
|
+
// it, and hops at zero is refused so that a relay chain invented later
|
|
61
|
+
// meets doors that already stop it. Nothing sets hops.
|
|
62
|
+
if (spent({ time: payload.time }) || payload.hops === 0)
|
|
63
|
+
return refuse();
|
|
59
64
|
const args = payload.args ?? {};
|
|
60
65
|
if (to === null) {
|
|
66
|
+
// For nobody: the public being, or D3 nobody home; D4 the signature
|
|
67
|
+
// fails under the key the payload names. She is asked by strangers,
|
|
68
|
+
// and a stranger hears silence for whatever she then does.
|
|
61
69
|
const pk = publicKey();
|
|
62
70
|
const pub = pk !== null ? doors.get(pk) : undefined;
|
|
63
71
|
if (!pub || !(await verifyAsk(a, payload.by)))
|
|
64
|
-
return
|
|
65
|
-
return { reply: await arrive(pub, {}, payload.method, args), ephemeralPk };
|
|
72
|
+
return refuse();
|
|
73
|
+
return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: true };
|
|
66
74
|
}
|
|
67
75
|
const h = heirs.admits(to, payload.by);
|
|
68
|
-
if (!h)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
if (!h) {
|
|
77
|
+
// D5 a heir not held, D6 a key not admitted. One more case: the heir
|
|
78
|
+
// was held and the id removed. Its last keys are kept so that their
|
|
79
|
+
// holder, and only their holder, D7 checked, hears that she is gone.
|
|
80
|
+
if (!heirs.gone(to, payload.by) || !(await verifyAsk(a, payload.by)))
|
|
81
|
+
return refuse();
|
|
82
|
+
return { reply: said('removed'), ephemeralPk, heard: true };
|
|
83
|
+
}
|
|
73
84
|
if (!(await verifyAsk(a, payload.by)))
|
|
74
|
-
return
|
|
85
|
+
return refuse(); // D7, under an admitted key
|
|
86
|
+
// From here the door has heard a key it holds. Every answer below is
|
|
87
|
+
// sealed to that key's lid; nothing below is a stranger's.
|
|
88
|
+
const door = doors.get(h.being);
|
|
89
|
+
if (!door)
|
|
90
|
+
return { reply: said('absent'), ephemeralPk, heard: true }; // D8. she did not come back this run
|
|
91
|
+
if (!at(door.cells.occupants, h.id))
|
|
92
|
+
return { reply: said('removed'), ephemeralPk, heard: true }; // D8. the record is gone
|
|
75
93
|
// Once only, and only now: the number is spent after the signature, so a
|
|
76
94
|
// stranger cannot burn a number she could not sign for, and together with
|
|
77
95
|
// the keys, so the same bytes twice rotate nothing and refusal writes nothing.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
const honoured = heirs.honour(h, payload.by, payload.next, payload.seq); // D9, D10
|
|
97
|
+
if (honoured !== true)
|
|
98
|
+
return { reply: said(honoured), ephemeralPk, heard: true };
|
|
99
|
+
return { reply: await arrive(door, { id: h.id }, payload.method, args, true), ephemeralPk, heard: true };
|
|
81
100
|
};
|
|
82
101
|
// The door itself. Always answers bytes. When the ask did not open, the
|
|
83
102
|
// reply is sealed to the ephemeral pk on its lid, which is all a stranger
|
|
@@ -89,11 +108,12 @@ export function makeDoor(key, heirs, doors, publicKey, random) {
|
|
|
89
108
|
return async function door(bytes) {
|
|
90
109
|
const out = await judge(bytes);
|
|
91
110
|
const reply = out?.reply ?? SILENCE;
|
|
111
|
+
const heard = out?.heard ?? false;
|
|
92
112
|
try {
|
|
93
|
-
return await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32));
|
|
113
|
+
return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
|
|
94
114
|
}
|
|
95
115
|
catch {
|
|
96
|
-
return sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32));
|
|
116
|
+
return { bytes: await sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
|
|
97
117
|
}
|
|
98
118
|
};
|
|
99
119
|
}
|
package/dist/ward/ground.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export type Ground = {
|
|
|
7
7
|
random(n: number): Uint8Array;
|
|
8
8
|
};
|
|
9
9
|
export type WardPointers = {
|
|
10
|
-
door(bytes: Uint8Array): Promise<
|
|
10
|
+
door(bytes: Uint8Array): Promise<{
|
|
11
|
+
bytes: Uint8Array;
|
|
12
|
+
heard: boolean;
|
|
13
|
+
}>;
|
|
11
14
|
ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
12
15
|
};
|
package/dist/ward/heirs.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DoorWord } from '../being/types.ts';
|
|
2
|
+
import { type Heir, type Partition } from './partition.ts';
|
|
2
3
|
export declare class Heirs {
|
|
3
4
|
#private;
|
|
4
5
|
constructor(p: Partition);
|
|
5
6
|
open(heir: string, being: string, id: string): void;
|
|
6
7
|
close(heir: string): void;
|
|
8
|
+
gone(heir: string, by: string): boolean;
|
|
7
9
|
get(heir: string): Heir | undefined;
|
|
8
10
|
admits(heir: string, by: string): Heir | null;
|
|
9
11
|
spend(h: Heir, seq: number): boolean;
|
|
10
|
-
honour(h: Heir, by: string, next: string | null, seq: number):
|
|
12
|
+
honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord;
|
|
11
13
|
settle(h: Heir, by: string, next: string | null): boolean;
|
|
12
14
|
}
|
package/dist/ward/heirs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GONE } from './partition.js';
|
|
1
2
|
// How wide the span is, is the ward's own — wider is more forgiving of a
|
|
2
3
|
// rough road, and no peer can tell the difference except by being refused.
|
|
3
4
|
const SPAN = 64;
|
|
@@ -9,8 +10,24 @@ export class Heirs {
|
|
|
9
10
|
open(heir, being, id) {
|
|
10
11
|
this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
|
|
11
12
|
}
|
|
13
|
+
// The id was removed. The heir is forgotten, and its last keys are kept
|
|
14
|
+
// apart, bounded, so that whoever still holds them hears `removed` at the
|
|
15
|
+
// door, once they have signed as themselves, and nobody else hears a thing.
|
|
12
16
|
close(heir) {
|
|
17
|
+
const h = this.#p.heirs[heir];
|
|
18
|
+
if (!h)
|
|
19
|
+
return;
|
|
13
20
|
delete this.#p.heirs[heir];
|
|
21
|
+
this.#p.gone[heir] = { current: h.current, announced: h.announced };
|
|
22
|
+
const keys = Object.keys(this.#p.gone);
|
|
23
|
+
for (const old of keys.slice(0, Math.max(0, keys.length - GONE)))
|
|
24
|
+
delete this.#p.gone[old];
|
|
25
|
+
}
|
|
26
|
+
// May `by` speak for a relation she removed? True only for the keys the
|
|
27
|
+
// door held when the id went, which nobody but their holder has.
|
|
28
|
+
gone(heir, by) {
|
|
29
|
+
const g = this.#p.gone[heir];
|
|
30
|
+
return !!g && (by === g.current || by === g.announced);
|
|
14
31
|
}
|
|
15
32
|
get(heir) {
|
|
16
33
|
return this.#p.heirs[heir];
|
|
@@ -60,10 +77,10 @@ export class Heirs {
|
|
|
60
77
|
// behind her. Every write below this line is one that is going to hold.
|
|
61
78
|
honour(h, by, next, seq) {
|
|
62
79
|
if (h.fresh && next === null)
|
|
63
|
-
return
|
|
80
|
+
return 'unannounced'; // a knock without a key of her own binds nothing
|
|
64
81
|
if (!this.spend(h, seq))
|
|
65
|
-
return
|
|
66
|
-
return this.settle(h, by, next);
|
|
82
|
+
return 'repeated';
|
|
83
|
+
return this.settle(h, by, next) ? true : 'unannounced';
|
|
67
84
|
}
|
|
68
85
|
// The signature checked out. Settle the keys: a fresh heir rotates at once
|
|
69
86
|
// to what it announced and must announce something; a current key replaces
|
package/dist/ward/owner.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// describe, boot, invite, knock, remove. The knock is a being's knock made
|
|
5
5
|
// for her, and the standing is written into her cells under the id the
|
|
6
6
|
// owner gave; remove takes a relation out of her by id, the mirror of it.
|
|
7
|
-
import { isSilence,
|
|
7
|
+
import { isSilence, isWord, wordOf } from '../being/silence.js';
|
|
8
8
|
import { digest } from '../being/digest.js';
|
|
9
9
|
import { OWNER } from '../being/types.js';
|
|
10
10
|
export const OWNER_ASKS = ['boot', 'invite', 'knock', 'remove'];
|
|
@@ -63,8 +63,10 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
63
63
|
// The owner is a caller like any other and may say what this knock may
|
|
64
64
|
// spend. Saying nothing is the ward's default, as it is for a being.
|
|
65
65
|
const out = await door.stance.standings.knock(inv, args.method, args.args ?? {}, args.wanted);
|
|
66
|
-
if (isSilence(out)
|
|
67
|
-
return { error:
|
|
66
|
+
if (isSilence(out))
|
|
67
|
+
return { error: 'silence' };
|
|
68
|
+
if (isWord(out))
|
|
69
|
+
return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
|
|
68
70
|
return { taken: await door.stance.standings.take(id, inv), answer: out };
|
|
69
71
|
}
|
|
70
72
|
if (method === 'remove') {
|
|
@@ -100,7 +102,7 @@ async function describe(w) {
|
|
|
100
102
|
let d = null;
|
|
101
103
|
try {
|
|
102
104
|
const bp = await door.being.answer({ id: OWNER });
|
|
103
|
-
d = isSilence(bp) ||
|
|
105
|
+
d = isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
104
106
|
}
|
|
105
107
|
catch {
|
|
106
108
|
// she threw where she was asked. there is no blueprint, and d stays null.
|
package/dist/ward/partition.d.ts
CHANGED
|
@@ -34,6 +34,11 @@ export type Bind = {
|
|
|
34
34
|
answered: Record<string, true>;
|
|
35
35
|
minted: string[];
|
|
36
36
|
};
|
|
37
|
+
export type Gone = {
|
|
38
|
+
current: string;
|
|
39
|
+
announced: string | null;
|
|
40
|
+
};
|
|
41
|
+
export declare const GONE = 256;
|
|
37
42
|
export type Partition = {
|
|
38
43
|
version: string;
|
|
39
44
|
beings: Record<string, Cells & {
|
|
@@ -41,6 +46,7 @@ export type Partition = {
|
|
|
41
46
|
}>;
|
|
42
47
|
bind: Record<string, Bind>;
|
|
43
48
|
heirs: Record<string, Heir>;
|
|
49
|
+
gone: Record<string, Gone>;
|
|
44
50
|
public: string | null;
|
|
45
51
|
};
|
|
46
52
|
export declare function open(memory: Record<string, unknown>): Partition;
|
package/dist/ward/partition.js
CHANGED
|
@@ -25,6 +25,7 @@ export const READS = [VERSION];
|
|
|
25
25
|
function upgrade(p) {
|
|
26
26
|
return p;
|
|
27
27
|
}
|
|
28
|
+
export const GONE = 256;
|
|
28
29
|
// The one place a ward reads state that another ward wrote. A fresh
|
|
29
30
|
// memory becomes a partition of this version; anything already written must
|
|
30
31
|
// say it is this version, and a ward that cannot read it does not boot. Birth is
|
|
@@ -41,6 +42,7 @@ export function open(memory) {
|
|
|
41
42
|
p.beings ??= {};
|
|
42
43
|
p.bind ??= {};
|
|
43
44
|
p.heirs ??= {};
|
|
45
|
+
p.gone ??= {};
|
|
44
46
|
p.public ??= null;
|
|
45
47
|
return p;
|
|
46
48
|
}
|
package/dist/ward/seal.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Json, JsonObject } from '../being/types.ts';
|
|
1
|
+
import type { DoorWord, Json, JsonObject } from '../being/types.ts';
|
|
2
2
|
export type WardKey = {
|
|
3
3
|
sign: Uint8Array;
|
|
4
4
|
padlock: Uint8Array;
|
|
@@ -28,6 +28,8 @@ export type ReplyPayload = {
|
|
|
28
28
|
seen: string | null;
|
|
29
29
|
} | {
|
|
30
30
|
silence: true;
|
|
31
|
+
} | {
|
|
32
|
+
quo: DoorWord;
|
|
31
33
|
};
|
|
32
34
|
export declare function sealAsk(to: string | null, payload: AskPayload, signer: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{
|
|
33
35
|
bytes: Uint8Array;
|