@quo-systems/quo 0.1.0 → 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/README.md +9 -6
- package/SPEC.md +235 -137
- package/dist/being/being.d.ts +22 -0
- package/dist/being/being.js +78 -0
- package/dist/being/digest.d.ts +3 -0
- package/dist/being/digest.js +19 -0
- package/dist/being/index.d.ts +5 -0
- package/dist/being/index.js +6 -0
- package/dist/being/silence.d.ts +10 -0
- package/dist/being/silence.js +29 -0
- package/dist/being/types.d.ts +84 -0
- package/dist/being/types.js +16 -0
- package/dist/conformance/assert.d.ts +11 -0
- package/dist/conformance/assert.js +72 -0
- package/dist/conformance/beings.d.ts +114 -0
- package/dist/conformance/beings.js +126 -0
- package/dist/conformance/estate.d.ts +5 -0
- package/dist/conformance/estate.js +316 -0
- package/dist/conformance/index.d.ts +65 -0
- package/dist/conformance/index.js +448 -0
- package/dist/conformance/reach.d.ts +10 -0
- package/dist/conformance/reach.js +72 -0
- package/dist/conformance/store.d.ts +5 -0
- package/dist/conformance/store.js +97 -0
- package/dist/harbor/core.d.ts +46 -0
- package/dist/harbor/core.js +218 -0
- package/dist/harbor/dial.d.ts +8 -0
- package/dist/harbor/dial.js +45 -0
- package/dist/harbor/index.d.ts +6 -0
- package/dist/harbor/index.js +10 -0
- package/dist/harbor/memory.d.ts +24 -0
- package/dist/harbor/memory.js +68 -0
- package/dist/harbor/reach.d.ts +36 -0
- package/dist/harbor/reach.js +166 -0
- package/dist/harbor/store.d.ts +33 -0
- package/dist/harbor/store.js +41 -0
- package/dist/ward/allowance.d.ts +10 -0
- package/dist/ward/allowance.js +60 -0
- package/dist/ward/arithmetic.d.ts +26 -0
- package/dist/ward/arithmetic.js +159 -0
- package/dist/ward/cells.d.ts +3 -0
- package/dist/ward/cells.js +79 -0
- package/dist/ward/door.d.ts +16 -0
- package/dist/ward/door.js +127 -0
- package/dist/ward/ground.d.ts +15 -0
- package/dist/ward/ground.js +1 -0
- package/dist/ward/heirs.d.ts +14 -0
- package/dist/ward/heirs.js +102 -0
- package/dist/ward/index.d.ts +8 -0
- package/dist/ward/index.js +10 -0
- package/dist/ward/owner.d.ts +32 -0
- package/dist/ward/owner.js +116 -0
- package/dist/ward/partition.d.ts +57 -0
- package/dist/ward/partition.js +64 -0
- package/dist/ward/seal.d.ts +50 -0
- package/dist/ward/seal.js +109 -0
- package/dist/ward/stance.d.ts +19 -0
- package/dist/ward/stance.js +259 -0
- package/dist/ward/ward.d.ts +2 -0
- package/dist/ward/ward.js +197 -0
- package/package.json +21 -29
- 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 +56 -6
- package/src/harbor/dial.ts +1 -1
- 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
|
@@ -33,12 +33,13 @@
|
|
|
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.ts';
|
|
36
|
-
import { isSilence, isUnreached } from '../being/silence.ts';
|
|
36
|
+
import { isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
|
|
37
37
|
import type { Invitation } from '../being/types.ts';
|
|
38
38
|
import { Member } from './beings.ts';
|
|
39
39
|
import type { Census, Handle, Runner, World } from './index.ts';
|
|
40
40
|
|
|
41
41
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
42
|
+
const said = (x: unknown, w: string): boolean => isWord(x) && wordOf(x) === w;
|
|
42
43
|
|
|
43
44
|
// One occupant arc, as the host holds it. `guest` is null until somebody
|
|
44
45
|
// knocks; `live` goes false when she is kicked. Both are the host's business
|
|
@@ -161,10 +162,11 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
161
162
|
assert.deepEqual(await acme.being.knock(back, 'ping'), { pong: 'acme' });
|
|
162
163
|
assert.equal(await acme.being.take('them', back), 'them');
|
|
163
164
|
|
|
164
|
-
// 5. acme kicks her. her standing is still hers to hold
|
|
165
|
-
// told
|
|
165
|
+
// 5. acme kicks her. her standing is still hers to hold, and she is not
|
|
166
|
+
// told until she asks: then the door, under the key it bound for her,
|
|
167
|
+
// says removed, and only to her.
|
|
166
168
|
acme.being.occupants.remove('partner-1');
|
|
167
|
-
assert.ok(
|
|
169
|
+
assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
|
|
168
170
|
assert.deepEqual(Object.keys(partner.cells.standings), ['the-client']); // she never learns
|
|
169
171
|
assert.deepEqual(await acme.being.standings.them!.ask('ping'), { pong: 'acme' }); // the other arc never moved
|
|
170
172
|
|
|
@@ -174,7 +176,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
174
176
|
assert.notEqual(second.heir, first.heir);
|
|
175
177
|
assert.deepEqual(await partner.being.knock(second, 'ping'), { pong: 'partner-2' });
|
|
176
178
|
assert.equal(await partner.being.take('the-client-again', second), 'the-client-again');
|
|
177
|
-
assert.ok(
|
|
179
|
+
assert.ok(said(await partner.being.standings['the-client']!.ask('ping'), 'removed'));
|
|
178
180
|
ledger(w.census(), undefined, 'the story');
|
|
179
181
|
});
|
|
180
182
|
|
|
@@ -221,6 +223,9 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
221
223
|
assert.deepEqual(back, { pong: s.id }, `${guest} knocking ${s.host}/${s.id}`);
|
|
222
224
|
s.guest = guest;
|
|
223
225
|
s.answered = true;
|
|
226
|
+
} else if (!s.live && (s.guest === null || (s.guest === guest && (hold === undefined || hold.live)))) {
|
|
227
|
+
// kicked, and she speaks under a key the door bound for this seat: she hears why
|
|
228
|
+
assert.ok(said(back, 'removed'), `${guest} knocking kicked ${s.host}/${s.id} should hear removed`);
|
|
224
229
|
} else assert.ok(isSilence(back), `${guest} knocking ${s.host}/${s.id} should be silence`);
|
|
225
230
|
return `${guest} knocks ${s.host}/${s.id}`;
|
|
226
231
|
});
|
|
@@ -304,7 +309,7 @@ export function estate(label: string, make: () => Promise<World>, { canDown, tes
|
|
|
304
309
|
}
|
|
305
310
|
const out = await standing!.ask('ping');
|
|
306
311
|
if (h.seat.live) assert.deepEqual(out, { pong: h.seat.id }, `step ${step}: ${h.owner}/${h.id} -> ${h.seat.host}/${h.seat.id}`);
|
|
307
|
-
else assert.ok(
|
|
312
|
+
else assert.ok(said(out, 'removed'), `step ${step}: ${h.owner}/${h.id} is kicked and should hear removed`);
|
|
308
313
|
}
|
|
309
314
|
};
|
|
310
315
|
|
package/src/conformance/index.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
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.ts';
|
|
22
|
-
import { silence, isSilence, isUnreached } from '../being/silence.ts';
|
|
22
|
+
import { silence, isSilence, isUnreached, isWord, wordOf } from '../being/silence.ts';
|
|
23
23
|
import { digest } from '../being/digest.ts';
|
|
24
24
|
import type { Asker, BeingClass, Cells, Invitation, Json, JsonObject } from '../being/types.ts';
|
|
25
25
|
import { Printer, Shop, Customer, Echo, Member } from './beings.ts';
|
|
@@ -54,6 +54,8 @@ export type World = {
|
|
|
54
54
|
export { Printer, Shop, Customer, Echo, Member };
|
|
55
55
|
|
|
56
56
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
57
|
+
// Her ward's word, as she receives it.
|
|
58
|
+
const said = (x: unknown, w: string): boolean => isWord(x) && wordOf(x) === w;
|
|
57
59
|
|
|
58
60
|
// The runner is handed in. Node passes `node:test`; a browser and an edge
|
|
59
61
|
// worker pass a shim of their own. The suite is the ward's truth, and the
|
|
@@ -100,12 +102,12 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
100
102
|
assert.equal(i1.ward, i2.ward);
|
|
101
103
|
});
|
|
102
104
|
|
|
103
|
-
t('remove before any knock: the heir is forgotten, a knock on it
|
|
105
|
+
t('remove before any knock: the heir is forgotten, and a knock on it by whoever holds it hears removed', async () => {
|
|
104
106
|
const { w, shop, alice } = await world();
|
|
105
107
|
const inv = await shop.being.invite();
|
|
106
108
|
shop.being.occupants.remove('cust1');
|
|
107
109
|
assert.equal(w.heir(inv), undefined);
|
|
108
|
-
assert.ok(
|
|
110
|
+
assert.ok(said(await alice.being.join(inv), 'removed'));
|
|
109
111
|
assert.deepEqual(alice.cells.standings, {});
|
|
110
112
|
});
|
|
111
113
|
|
|
@@ -189,16 +191,16 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
189
191
|
assert.deepEqual(await alice.being.join(inv), { welcome: true }); // the same invitation still works
|
|
190
192
|
const inv2 = await shop.being.invite();
|
|
191
193
|
shop.being.occupants.remove('cust2'); // she changed her mind before the knock
|
|
192
|
-
assert.ok(
|
|
194
|
+
assert.ok(said(await alice.being.join(inv2), 'removed'));
|
|
193
195
|
assert.deepEqual(Object.keys(alice.cells.standings), ['shop']);
|
|
194
196
|
});
|
|
195
197
|
|
|
196
|
-
t('take without a knock, or after a
|
|
198
|
+
t('take without a knock, or after a knock that was not answered, births nothing', async () => {
|
|
197
199
|
const { shop, alice } = await world();
|
|
198
200
|
const inv = await shop.being.invite();
|
|
199
201
|
assert.equal(await alice.being.take('shop', inv), null);
|
|
200
202
|
shop.being.occupants.remove('cust1');
|
|
201
|
-
assert.ok(
|
|
203
|
+
assert.ok(said(await alice.being.knock(inv, 'hello'), 'removed'));
|
|
202
204
|
assert.equal(await alice.being.take('shop', inv), null);
|
|
203
205
|
assert.deepEqual(alice.cells.standings, {});
|
|
204
206
|
});
|
|
@@ -374,16 +376,16 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
374
376
|
assert.deepEqual(await alice.being.buy('hat'), { ok: true, receipt: 'receipt for hat' });
|
|
375
377
|
});
|
|
376
378
|
|
|
377
|
-
t('a removed occupant
|
|
379
|
+
t('a removed occupant hears removed under the key the door bound, her heir is forgotten; a dropped standing is gone', async () => {
|
|
378
380
|
const { w, alice, shop } = await world();
|
|
379
381
|
const inv = await shop.being.invite();
|
|
380
382
|
await alice.being.join(inv);
|
|
381
383
|
shop.being.occupants.remove('cust1');
|
|
382
384
|
assert.equal(w.heir(inv), undefined);
|
|
383
|
-
assert.ok(
|
|
385
|
+
assert.ok(said(await alice.being.buy('hat'), 'removed'));
|
|
384
386
|
alice.being.standings.remove('shop');
|
|
385
387
|
assert.equal(alice.being.standings.shop, undefined);
|
|
386
|
-
assert.ok(isSilence(await alice.being.knock(inv, 'hello'))); //
|
|
388
|
+
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
|
|
387
389
|
const again = await shop.being.invite(); // cust2. a new heir
|
|
388
390
|
assert.notEqual(again.heir, inv.heir);
|
|
389
391
|
assert.deepEqual(await alice.being.knock(again, 'hello'), { welcome: true });
|
|
@@ -398,7 +400,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
398
400
|
assert.equal((shop.cells.sales as unknown[]).length, 1);
|
|
399
401
|
});
|
|
400
402
|
|
|
401
|
-
t('a being that throws is
|
|
403
|
+
t('a being that throws is the word threw to whom the door has bound; the ward stays up', async () => {
|
|
402
404
|
const w = await make();
|
|
403
405
|
class Bomb {
|
|
404
406
|
s: unknown;
|
|
@@ -413,7 +415,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
413
415
|
const x = await w.boot<Bomb>('X', Bomb);
|
|
414
416
|
const a = await w.boot<Customer>('A', Customer);
|
|
415
417
|
const inv = (await (x.being.s as Customer['stance']).occupants.invite('a'))!;
|
|
416
|
-
assert.ok(
|
|
418
|
+
assert.ok(said(await a.being.knock(inv, 'any'), 'threw'));
|
|
417
419
|
assert.deepEqual(await a.being.knock(inv), { asks: [], notes: {} });
|
|
418
420
|
});
|
|
419
421
|
|
|
@@ -473,7 +475,7 @@ export function conform(label: string, make: () => Promise<World>, { canDown = t
|
|
|
473
475
|
assert.deepEqual(pub.cells.occupants, {}); // she never invited anyone
|
|
474
476
|
if (!w.oneWard) assert.ok(isSilence(await a.being.knock(at(priv), 'time'))); // a ward with no public being: silence
|
|
475
477
|
assert.ok(isUnreached(await a.being.knock({ ward: 'a'.repeat(128) }, 'time'))); // no door anywhere: unreached
|
|
476
|
-
assert.ok(
|
|
478
|
+
assert.ok(said(await a.being.knock({ ward: 'not a pk' }, 'time'), 'invitation')); // not even an address
|
|
477
479
|
assert.equal(await a.being.take('clock', at(pub)), 'clock');
|
|
478
480
|
assert.deepEqual(await a.being.standings.clock!.ask('time'), { time: 12, got: {} });
|
|
479
481
|
assert.equal(a.cells.standings.clock.seen, await digest((await a.being.standings.clock!.ask()) as Json));
|
package/src/harbor/core.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// tree, and every one of them passes the conformance suite untouched.
|
|
10
10
|
//
|
|
11
11
|
// The directory is filled three ways, in this order: the harbor's own
|
|
12
|
-
// doors; a socket a dialer holds to it, bound
|
|
13
|
-
//
|
|
12
|
+
// doors; a socket a dialer holds to it, bound once the door behind the
|
|
13
|
+
// dialer's claim has proved it holds the key, and unbound at close; a hint
|
|
14
|
+
// from a link. And one fallback: a dialer with nothing in its
|
|
14
15
|
// directory for a pk sends down the socket it holds, because the listener
|
|
15
16
|
// it dialed is the rendezvous and may hold that pk on another socket. Bytes
|
|
16
17
|
// that arrive from the wire go to an own door or a held socket and never
|
|
@@ -19,6 +20,9 @@ import { Ward } from '../ward/ward.ts';
|
|
|
19
20
|
import type { Ground, WardPointers } from '../ward/ground.ts';
|
|
20
21
|
import type { BeingClass, BeingLike } from '../being/types.ts';
|
|
21
22
|
import { request, type Reach } from './reach.ts';
|
|
23
|
+
import { openReply, wardSignPk } from '../ward/seal.ts';
|
|
24
|
+
import { concat, sealingPair, KEY } from '../ward/arithmetic.ts';
|
|
25
|
+
import { within, LATE } from '../ward/allowance.ts';
|
|
22
26
|
import type { Kept, Store, WardRecord } from './store.ts';
|
|
23
27
|
|
|
24
28
|
export type Hosted = WardPointers & { pk: string; name: string; record: WardRecord; partition: Record<string, unknown>; being(key: string): BeingLike | undefined; save(): Promise<void> };
|
|
@@ -30,6 +34,9 @@ export type Bound = { reach: Reach; held: boolean };
|
|
|
30
34
|
export type Loader = (record: WardRecord) => Promise<Record<string, BeingClass>>;
|
|
31
35
|
|
|
32
36
|
export const DEFAULT_CODE = 'classes/index.ts';
|
|
37
|
+
// How long a probe waits for the door behind a claim. A claim that answers
|
|
38
|
+
// nothing in this time is not bound; the next announce is another chance.
|
|
39
|
+
const PROBE = 10_000;
|
|
33
40
|
|
|
34
41
|
export class Harbor {
|
|
35
42
|
readonly store: Store;
|
|
@@ -38,6 +45,7 @@ export class Harbor {
|
|
|
38
45
|
readonly doors = new Map<string, WardPointers['door']>(); // ward pk -> door
|
|
39
46
|
readonly reaches = new Map<string, Bound>(); // the directory: foreign ward pk -> reach
|
|
40
47
|
readonly down = new Set<string>(); // pks this harbor will not reach right now: weather, for a test
|
|
48
|
+
readonly refused = new Map<string, number>(); // own ward pk -> arrivals its door would not admit. what a terrain does with it is its own
|
|
41
49
|
readonly classes: Record<string, BeingClass> = {}; // classes handed in-process, beside the loader's
|
|
42
50
|
// The dialers' sockets, in the order they were dialed: where a pk nobody
|
|
43
51
|
// here knows is sent, because the listener a harbor dialed is a rendezvous
|
|
@@ -64,10 +72,18 @@ export class Harbor {
|
|
|
64
72
|
|
|
65
73
|
// ---- the directory
|
|
66
74
|
|
|
75
|
+
// Bytes to one of this harbor's own doors, and the one bit the door says
|
|
76
|
+
// beside its reply, counted.
|
|
77
|
+
async #own(pk: string, door: WardPointers['door'], bytes: Uint8Array): Promise<Uint8Array> {
|
|
78
|
+
const r = await door(bytes);
|
|
79
|
+
if (!r.heard) this.refused.set(pk, (this.refused.get(pk) ?? 0) + 1);
|
|
80
|
+
return new Uint8Array(r.bytes);
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
async carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
68
84
|
if (this.down.has(pk)) return undefined;
|
|
69
85
|
const door = this.doors.get(pk);
|
|
70
|
-
if (door) return
|
|
86
|
+
if (door) return this.#own(pk, door, bytes);
|
|
71
87
|
const bound = this.reaches.get(pk);
|
|
72
88
|
if (bound) return bound.reach.carry(pk, bytes);
|
|
73
89
|
for (const reach of this.fallbacks) {
|
|
@@ -80,7 +96,7 @@ export class Harbor {
|
|
|
80
96
|
async deliver(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
81
97
|
if (this.down.has(pk)) return undefined;
|
|
82
98
|
const door = this.doors.get(pk);
|
|
83
|
-
if (door) return
|
|
99
|
+
if (door) return this.#own(pk, door, bytes);
|
|
84
100
|
const bound = this.reaches.get(pk);
|
|
85
101
|
return bound?.held ? bound.reach.carry(pk, bytes) : undefined;
|
|
86
102
|
}
|
|
@@ -95,8 +111,42 @@ export class Harbor {
|
|
|
95
111
|
await this.store.hint(pk, url);
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
// An announce is a claim, and a claim binds nothing until proven: anyone
|
|
115
|
+
// who can reach a listener could otherwise name a pk that is not theirs and
|
|
116
|
+
// take its reachability. The proof is the door itself, as it already is. A
|
|
117
|
+
// lid this harbor minted and noise after it is a box that does not open,
|
|
118
|
+
// and the door answers one such box with silence sealed to the lid and
|
|
119
|
+
// signed by the ward key. Only the holder of that ward's seed can write
|
|
120
|
+
// that reply, the lid is fresh so nothing replays, and a box that does not
|
|
121
|
+
// open writes nothing at the ward. This is the one box a harbor ever opens,
|
|
122
|
+
// the one it sealed itself, and it reads nothing from it but that the
|
|
123
|
+
// signature is the claimed key's.
|
|
124
|
+
async prove(pk: string, reach: Reach): Promise<boolean> {
|
|
125
|
+
if (!/^[0-9a-f]{128}$/.test(pk)) return false;
|
|
126
|
+
try {
|
|
127
|
+
const random = (n: number) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
128
|
+
const lid = await sealingPair(random(KEY));
|
|
129
|
+
const back = await within(PROBE, reach.carry(pk, concat([lid.pk, random(KEY)])));
|
|
130
|
+
if (back === LATE || back === undefined) return false;
|
|
131
|
+
const reply = await openReply(back, lid.secret, wardSignPk(pk));
|
|
132
|
+
return reply !== null && 'silence' in reply;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Bind what the far side claims, each pk proven at its door first. Both
|
|
139
|
+
// sides bind this way: the listener the dialer's claims, the dialer the
|
|
140
|
+
// listener's, since a claim is a claim whichever end made it.
|
|
141
|
+
async bind(pks: string[], reach: Reach, held: boolean): Promise<void> {
|
|
142
|
+
// An announce is the whole of what that side holds now, so a pk this
|
|
143
|
+
// reach was bound for and no longer claims is unbound at once: a ward
|
|
144
|
+
// that left a dialer is not reachable through it, proof or no proof.
|
|
145
|
+
for (const [pk, b] of this.reaches) if (b.reach === reach && !pks.includes(pk)) this.reaches.delete(pk);
|
|
146
|
+
await Promise.all(pks.map(async (pk) => {
|
|
147
|
+
if (this.doors.has(pk) || !(await this.prove(pk, reach))) return;
|
|
148
|
+
this.reaches.set(pk, { reach, held });
|
|
149
|
+
}));
|
|
100
150
|
}
|
|
101
151
|
unbind(reach: Reach): void {
|
|
102
152
|
for (const [pk, b] of this.reaches) if (b.reach === reach) this.reaches.delete(pk);
|
package/src/harbor/dial.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function dial(harbor: Harbor, url: string): Dialer {
|
|
|
30
30
|
// not at the handshake: a listener that opens and then refuses every
|
|
31
31
|
// time would otherwise be dialed at a fixed two seconds for good.
|
|
32
32
|
wait = 1000;
|
|
33
|
-
harbor.bind(far, s, false); // the listener's
|
|
33
|
+
void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
|
|
34
34
|
},
|
|
35
35
|
(why) => {
|
|
36
36
|
harbor.unbind(s);
|
package/src/harbor/memory.ts
CHANGED
|
@@ -15,6 +15,7 @@ export class MemoryHarbor {
|
|
|
15
15
|
readonly doors = new Map<string, WardPointers['door']>(); // ward pk -> door
|
|
16
16
|
readonly down = new Set<string>(); // ward pks this harbor cannot reach right now
|
|
17
17
|
readonly wire: string[] = []; // every byte string that ever crossed, as hex. for inspection
|
|
18
|
+
readonly refused = new Map<string, number>(); // ward pk -> arrivals its door would not admit: strangers, counted
|
|
18
19
|
readonly peers = new Set<MemoryHarbor>();
|
|
19
20
|
cut = false;
|
|
20
21
|
readonly partitions = new Map<string, Record<string, unknown>>(); // seed -> memory. the harbor keeps it and reads nothing
|
|
@@ -25,7 +26,11 @@ export class MemoryHarbor {
|
|
|
25
26
|
async route(farPk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> {
|
|
26
27
|
if (this.down.has(farPk)) return undefined;
|
|
27
28
|
const door = this.doors.get(farPk);
|
|
28
|
-
if (door)
|
|
29
|
+
if (door) {
|
|
30
|
+
const r = await door(bytes);
|
|
31
|
+
if (!r.heard) this.refused.set(farPk, (this.refused.get(farPk) ?? 0) + 1);
|
|
32
|
+
return r.bytes;
|
|
33
|
+
}
|
|
29
34
|
const peer = [...this.peers].find((h) => h.doors.has(farPk) && !h.down.has(farPk));
|
|
30
35
|
if (!peer || this.cut) return undefined; // nothing. the harbor has no word silence
|
|
31
36
|
return peer.route(farPk, bytes);
|
package/src/ward/door.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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.ts';
|
|
7
11
|
import { digest } from '../being/digest.ts';
|
|
8
|
-
import type { Asker, BeingLike, JsonObject } from '../being/types.ts';
|
|
12
|
+
import type { Asker, BeingLike, DoorWord, JsonObject } from '../being/types.ts';
|
|
9
13
|
import { at } from './partition.ts';
|
|
10
14
|
import type { Heirs } from './heirs.ts';
|
|
11
15
|
import { openAsk, sealReply, verifyAsk, type ReplyPayload, type WardKey } from './seal.ts';
|
|
@@ -13,20 +17,26 @@ import { spent } from './allowance.ts';
|
|
|
13
17
|
import { KEY, sealingPair } from './arithmetic.ts';
|
|
14
18
|
|
|
15
19
|
export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, unknown> } };
|
|
20
|
+
export type Judged = { bytes: Uint8Array; heard: boolean };
|
|
16
21
|
const SILENCE: ReplyPayload = { silence: true };
|
|
22
|
+
const said = (quo: DoorWord): ReplyPayload => ({ quo });
|
|
17
23
|
|
|
18
|
-
// One arrival at one being, already named. Catches every throw.
|
|
19
|
-
|
|
24
|
+
// One arrival at one being, already named. Catches every throw. `bound` says
|
|
25
|
+
// whether the asker is a key this door holds: she hears `threw`; a stranger
|
|
26
|
+
// at the public being hears silence, because her insides are hers.
|
|
27
|
+
export async function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload> {
|
|
28
|
+
const threw = bound ? said('threw') : SILENCE;
|
|
20
29
|
let out: Awaited<ReturnType<BeingLike['answer']>>;
|
|
21
30
|
try {
|
|
22
31
|
out = await door.being.answer(asker, method, args);
|
|
23
32
|
} catch {
|
|
24
|
-
return
|
|
33
|
+
return threw; // she threw where she was asked. there is no answer.
|
|
25
34
|
}
|
|
26
35
|
// Nothing at all is not an answer either: a method that forgot to return
|
|
27
36
|
// has said nothing, and nothing is silence, never a value that JSON drops
|
|
28
37
|
// on the way out and the far side reads back as a fourth word.
|
|
29
|
-
if (out === undefined || isSilence(out)
|
|
38
|
+
if (out === undefined || isSilence(out)) return SILENCE;
|
|
39
|
+
if (isWord(out)) return threw; // a word is the ward's to say, never hers
|
|
30
40
|
if (method === undefined) return { object: out, seen: null };
|
|
31
41
|
// The digest rides along, it is not the answer. She has already answered:
|
|
32
42
|
// a describe that will not run costs the digest, and nothing else.
|
|
@@ -36,43 +46,54 @@ export async function arrive(door: Door, asker: Asker, method: string | undefine
|
|
|
36
46
|
async function seen(door: Door, asker: Asker): Promise<string | null> {
|
|
37
47
|
try {
|
|
38
48
|
const bp = await door.being.answer(asker);
|
|
39
|
-
return isSilence(bp) ||
|
|
49
|
+
return isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
40
50
|
} catch {
|
|
41
51
|
return null;
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
type Verdict = { reply: ReplyPayload; ephemeralPk: Uint8Array; heard: boolean };
|
|
56
|
+
|
|
45
57
|
export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array) {
|
|
46
|
-
const judge = async (bytes: Uint8Array): Promise<
|
|
58
|
+
const judge = async (bytes: Uint8Array): Promise<Verdict | null> => {
|
|
47
59
|
const a = await openAsk(bytes, key.padlock);
|
|
48
|
-
if (!a) return null; // it did not open. there is nobody to answer.
|
|
60
|
+
if (!a) return null; // D1. it did not open. there is nobody to answer.
|
|
49
61
|
const { to, payload, ephemeralPk } = a;
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
if (spent({ time: payload.time })
|
|
55
|
-
// An ask with no doors left is one silence like a budget already gone.
|
|
56
|
-
// Nothing sets hops and nothing decrements it yet; this door refuses zero
|
|
57
|
-
// so that a relay chain invented later meets doors that already stop it.
|
|
58
|
-
if (payload.hops === 0) return { reply: SILENCE, ephemeralPk };
|
|
62
|
+
const refuse = (): Verdict => ({ reply: SILENCE, ephemeralPk, heard: false });
|
|
63
|
+
// D2, the rest of it. The allowance is read before anything is done under
|
|
64
|
+
// it, and hops at zero is refused so that a relay chain invented later
|
|
65
|
+
// meets doors that already stop it. Nothing sets hops.
|
|
66
|
+
if (spent({ time: payload.time }) || payload.hops === 0) return refuse();
|
|
59
67
|
const args = payload.args ?? {};
|
|
60
68
|
if (to === null) {
|
|
69
|
+
// For nobody: the public being, or D3 nobody home; D4 the signature
|
|
70
|
+
// fails under the key the payload names. She is asked by strangers,
|
|
71
|
+
// and a stranger hears silence for whatever she then does.
|
|
61
72
|
const pk = publicKey();
|
|
62
73
|
const pub = pk !== null ? doors.get(pk) : undefined;
|
|
63
|
-
if (!pub || !(await verifyAsk(a, payload.by))) return
|
|
64
|
-
return { reply: await arrive(pub, {}, payload.method, args), ephemeralPk };
|
|
74
|
+
if (!pub || !(await verifyAsk(a, payload.by))) return refuse();
|
|
75
|
+
return { reply: await arrive(pub, {}, payload.method, args, false), ephemeralPk, heard: true };
|
|
65
76
|
}
|
|
66
77
|
const h = heirs.admits(to, payload.by);
|
|
67
|
-
if (!h)
|
|
78
|
+
if (!h) {
|
|
79
|
+
// D5 a heir not held, D6 a key not admitted. One more case: the heir
|
|
80
|
+
// was held and the id removed. Its last keys are kept so that their
|
|
81
|
+
// holder, and only their holder, D7 checked, hears that she is gone.
|
|
82
|
+
if (!heirs.gone(to, payload.by) || !(await verifyAsk(a, payload.by))) return refuse();
|
|
83
|
+
return { reply: said('removed'), ephemeralPk, heard: true };
|
|
84
|
+
}
|
|
85
|
+
if (!(await verifyAsk(a, payload.by))) 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.
|
|
68
88
|
const door = doors.get(h.being);
|
|
69
|
-
if (!door
|
|
70
|
-
if (!(
|
|
89
|
+
if (!door) return { reply: said('absent'), ephemeralPk, heard: true }; // D8. she did not come back this run
|
|
90
|
+
if (!at(door.cells.occupants, h.id)) return { reply: said('removed'), ephemeralPk, heard: true }; // D8. the record is gone
|
|
71
91
|
// Once only, and only now: the number is spent after the signature, so a
|
|
72
92
|
// stranger cannot burn a number she could not sign for, and together with
|
|
73
93
|
// the keys, so the same bytes twice rotate nothing and refusal writes nothing.
|
|
74
|
-
|
|
75
|
-
|
|
94
|
+
const honoured = heirs.honour(h, payload.by, payload.next, payload.seq); // D9, D10
|
|
95
|
+
if (honoured !== true) return { reply: said(honoured), ephemeralPk, heard: true };
|
|
96
|
+
return { reply: await arrive(door, { id: h.id }, payload.method, args, true), ephemeralPk, heard: true };
|
|
76
97
|
};
|
|
77
98
|
|
|
78
99
|
// The door itself. Always answers bytes. When the ask did not open, the
|
|
@@ -82,13 +103,14 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
|
|
|
82
103
|
// A lid that is not a key -- a small-order point, of which the two curves
|
|
83
104
|
// have several each -- makes a dead agreement, and the seal refuses it. The
|
|
84
105
|
// door never throws: that reply is noise, sealed to a key nobody holds.
|
|
85
|
-
return async function door(bytes: Uint8Array): Promise<
|
|
106
|
+
return async function door(bytes: Uint8Array): Promise<Judged> {
|
|
86
107
|
const out = await judge(bytes);
|
|
87
108
|
const reply = out?.reply ?? SILENCE;
|
|
109
|
+
const heard = out?.heard ?? false;
|
|
88
110
|
try {
|
|
89
|
-
return await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32));
|
|
111
|
+
return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
|
|
90
112
|
} catch {
|
|
91
|
-
return sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32));
|
|
113
|
+
return { bytes: await sealReply(reply, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
|
|
92
114
|
}
|
|
93
115
|
};
|
|
94
116
|
}
|
package/src/ward/ground.ts
CHANGED
|
@@ -28,8 +28,12 @@ export type Ground = {
|
|
|
28
28
|
random(n: number): Uint8Array; // entropy. every key a ward mints is drawn from it
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
// What a ward hands back. Two pointers.
|
|
31
|
+
// What a ward hands back. Two pointers. The door answers bytes, always, and
|
|
32
|
+
// one bit beside them: whether a key it holds spoke. That is all a harbor
|
|
33
|
+
// learns from an arrival, and it is what a harbor can act on -- a pk that
|
|
34
|
+
// only ever brings strangers' bytes is the harbor's to rate or refuse. Never
|
|
35
|
+
// a reason: the reason is sealed to the asker's lid, and is theirs.
|
|
32
36
|
export type WardPointers = {
|
|
33
|
-
door(bytes: Uint8Array): Promise<Uint8Array>;
|
|
37
|
+
door(bytes: Uint8Array): Promise<{ bytes: Uint8Array; heard: boolean }>;
|
|
34
38
|
ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
35
39
|
};
|
package/src/ward/heirs.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// speak, and so may the key it announced last time. A knock is the first
|
|
5
5
|
// use of the heir, which the inviter announced on your behalf, and the heir
|
|
6
6
|
// dies as it speaks.
|
|
7
|
-
import type {
|
|
7
|
+
import type { DoorWord } from '../being/types.ts';
|
|
8
|
+
import { GONE, type Heir, type Partition } from './partition.ts';
|
|
8
9
|
|
|
9
10
|
// How wide the span is, is the ward's own — wider is more forgiving of a
|
|
10
11
|
// rough road, and no peer can tell the difference except by being refused.
|
|
@@ -18,8 +19,22 @@ export class Heirs {
|
|
|
18
19
|
open(heir: string, being: string, id: string): void {
|
|
19
20
|
this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
|
|
20
21
|
}
|
|
22
|
+
// The id was removed. The heir is forgotten, and its last keys are kept
|
|
23
|
+
// apart, bounded, so that whoever still holds them hears `removed` at the
|
|
24
|
+
// door, once they have signed as themselves, and nobody else hears a thing.
|
|
21
25
|
close(heir: string): void {
|
|
26
|
+
const h = this.#p.heirs[heir];
|
|
27
|
+
if (!h) return;
|
|
22
28
|
delete this.#p.heirs[heir];
|
|
29
|
+
this.#p.gone[heir] = { current: h.current, announced: h.announced };
|
|
30
|
+
const keys = Object.keys(this.#p.gone);
|
|
31
|
+
for (const old of keys.slice(0, Math.max(0, keys.length - GONE))) delete this.#p.gone[old];
|
|
32
|
+
}
|
|
33
|
+
// May `by` speak for a relation she removed? True only for the keys the
|
|
34
|
+
// door held when the id went, which nobody but their holder has.
|
|
35
|
+
gone(heir: string, by: string): boolean {
|
|
36
|
+
const g = this.#p.gone[heir];
|
|
37
|
+
return !!g && (by === g.current || by === g.announced);
|
|
23
38
|
}
|
|
24
39
|
get(heir: string): Heir | undefined {
|
|
25
40
|
return this.#p.heirs[heir];
|
|
@@ -63,10 +78,10 @@ export class Heirs {
|
|
|
63
78
|
// neither: a call that binds nothing must not burn a number on its way to
|
|
64
79
|
// being refused, or a stranger who cannot be heard would still leave a mark
|
|
65
80
|
// behind her. Every write below this line is one that is going to hold.
|
|
66
|
-
honour(h: Heir, by: string, next: string | null, seq: number):
|
|
67
|
-
if (h.fresh && next === null) return
|
|
68
|
-
if (!this.spend(h, seq)) return
|
|
69
|
-
return this.settle(h, by, next);
|
|
81
|
+
honour(h: Heir, by: string, next: string | null, seq: number): true | DoorWord {
|
|
82
|
+
if (h.fresh && next === null) return 'unannounced'; // a knock without a key of her own binds nothing
|
|
83
|
+
if (!this.spend(h, seq)) return 'repeated';
|
|
84
|
+
return this.settle(h, by, next) ? true : 'unannounced';
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
// The signature checked out. Settle the keys: a fresh heir rotates at once
|
package/src/ward/owner.ts
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.ts';
|
|
8
8
|
import { digest } from '../being/digest.ts';
|
|
9
9
|
import { OWNER } from '../being/types.ts';
|
|
10
10
|
import type { Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
|
|
@@ -78,7 +78,8 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
78
78
|
// The owner is a caller like any other and may say what this knock may
|
|
79
79
|
// spend. Saying nothing is the ward's default, as it is for a being.
|
|
80
80
|
const out = await door.stance.standings.knock(inv, args.method as string | undefined, (args.args as JsonObject) ?? {}, args.wanted as Wanted | undefined);
|
|
81
|
-
if (isSilence(out)
|
|
81
|
+
if (isSilence(out)) return { error: 'silence' };
|
|
82
|
+
if (isWord(out)) return { error: wordOf(out) }; // the owner hears objects: the word is the error's name
|
|
82
83
|
return { taken: await door.stance.standings.take(id, inv), answer: out as Json };
|
|
83
84
|
}
|
|
84
85
|
if (method === 'remove') {
|
|
@@ -109,7 +110,7 @@ async function describe(w: OwnerSide): Promise<Json> {
|
|
|
109
110
|
let d: string | null = null;
|
|
110
111
|
try {
|
|
111
112
|
const bp = await (door.being.answer as (a: unknown) => unknown)({ id: OWNER });
|
|
112
|
-
d = isSilence(bp) ||
|
|
113
|
+
d = isSilence(bp) || isWord(bp) ? null : await digest(bp as Json);
|
|
113
114
|
} catch {
|
|
114
115
|
// she threw where she was asked. there is no blueprint, and d stays null.
|
|
115
116
|
}
|
package/src/ward/partition.ts
CHANGED
|
@@ -78,11 +78,20 @@ export type Bind = {
|
|
|
78
78
|
minted: string[]; // the last eight pks her side minted. inner. a test reads it
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// A relation she removed, as the door remembers it: the keys that may still
|
|
82
|
+
// speak for it, so their holder hears `removed` and a stranger still hears
|
|
83
|
+
// silence. Nothing else survives the removal. The list is bounded, oldest
|
|
84
|
+
// out, because a door that remembered every relation ever ended would grow
|
|
85
|
+
// for as long as she keeps inviting.
|
|
86
|
+
export type Gone = { current: string; announced: string | null };
|
|
87
|
+
export const GONE = 256;
|
|
88
|
+
|
|
81
89
|
export type Partition = {
|
|
82
90
|
version: string;
|
|
83
91
|
beings: Record<string, Cells & { class?: string }>; // key -> her cells
|
|
84
92
|
bind: Record<string, Bind>; // key -> her bind table
|
|
85
93
|
heirs: Record<string, Heir>; // heir pk -> occupant, across every being of the ward
|
|
94
|
+
gone: Record<string, Gone>; // heir pk -> the last keys of a relation she removed
|
|
86
95
|
public: string | null; // the one public being's key
|
|
87
96
|
};
|
|
88
97
|
|
|
@@ -101,6 +110,7 @@ export function open(memory: Record<string, unknown>): Partition {
|
|
|
101
110
|
p.beings ??= {};
|
|
102
111
|
p.bind ??= {};
|
|
103
112
|
p.heirs ??= {};
|
|
113
|
+
p.gone ??= {};
|
|
104
114
|
p.public ??= null;
|
|
105
115
|
return p;
|
|
106
116
|
}
|
package/src/ward/seal.ts
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
// itself. The door opens every ask with its own padlock and reads `to` after,
|
|
19
19
|
// so nothing was ever gained by having it in the clear.
|
|
20
20
|
// reply on the wire box = eph X pk || AESGCM( reply || signature ) sealed to the ask's eph pk
|
|
21
|
-
// reply = JSON { object, seen } | { silence: true } signed by the ward key
|
|
22
|
-
import type { Json, JsonObject } from '../being/types.ts';
|
|
21
|
+
// reply = JSON { object, seen } | { silence: true } | { quo: word } signed by the ward key
|
|
22
|
+
import type { DoorWord, Json, JsonObject } from '../being/types.ts';
|
|
23
|
+
import { isDoorWord } from '../being/silence.ts';
|
|
23
24
|
import { KEY, SIGNATURE, box, concat, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.ts';
|
|
24
25
|
|
|
25
26
|
const utf8 = new TextEncoder();
|
|
@@ -58,7 +59,7 @@ export type AskPayload = { by: string; next: string | null; seq: number; time: n
|
|
|
58
59
|
// What is actually sealed: the payload with the heir it is for, or null for
|
|
59
60
|
// the public being. `to` is signed with the rest, so it cannot be moved.
|
|
60
61
|
type Sealed = AskPayload & { to: string | null };
|
|
61
|
-
export type ReplyPayload = { object: Json; seen: string | null } | { silence: true };
|
|
62
|
+
export type ReplyPayload = { object: Json; seen: string | null } | { silence: true } | { quo: DoorWord };
|
|
62
63
|
|
|
63
64
|
// Seal an ask. Returns the bytes and the ephemeral secret the reply will be sealed to.
|
|
64
65
|
export async function sealAsk(to: string | null, payload: AskPayload, signer: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{ bytes: Uint8Array; ephemeral: Uint8Array }> {
|
|
@@ -109,8 +110,9 @@ export async function sealReply(reply: ReplyPayload, ephemeralPk: Uint8Array, wa
|
|
|
109
110
|
// Open a reply with the ephemeral secret kept from the ask, and verify it
|
|
110
111
|
// came from the ward it was sent to. Null is silence: it did not open, it
|
|
111
112
|
// lied, or it is not a reply. Every ward on the wire is somebody else's code,
|
|
112
|
-
// so the shape is checked here and nowhere later: a reply is `{ silence }
|
|
113
|
-
//
|
|
113
|
+
// so the shape is checked here and nowhere later: a reply is `{ silence }`,
|
|
114
|
+
// `{ quo: word }` with a word the door may say, or `{ object, seen }` with an
|
|
115
|
+
// object present and seen a digest or null.
|
|
114
116
|
// Anything else is bytes that said nothing, and her ward never hands her a
|
|
115
117
|
// throw or an absent value for what a stranger wrote.
|
|
116
118
|
export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, signPk: Uint8Array): Promise<ReplyPayload | null> {
|
|
@@ -124,6 +126,7 @@ export async function openReply(bytes: unknown, ephemeralSecret: Uint8Array, sig
|
|
|
124
126
|
if (reply === null || typeof reply !== 'object' || Array.isArray(reply)) return null;
|
|
125
127
|
const r = reply as Record<string, unknown>;
|
|
126
128
|
if (r.silence === true) return { silence: true };
|
|
129
|
+
if (Object.hasOwn(r, 'quo')) return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
127
130
|
if (!Object.hasOwn(r, 'object') || r.object === undefined) return null;
|
|
128
131
|
if (r.seen !== null && !(typeof r.seen === 'string' && /^[0-9a-f]{64}$/.test(r.seen))) return null;
|
|
129
132
|
return { object: r.object as Json, seen: r.seen };
|