@quo-systems/quo 0.2.9 → 0.2.10
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 +19 -10
- package/SPEC.md +161 -89
- package/dist/being/being.d.ts +2 -2
- package/dist/being/being.js +27 -11
- package/dist/being/digest.js +11 -7
- package/dist/being/types.d.ts +2 -2
- package/dist/conformance/estate.js +1 -3
- package/dist/conformance/index.js +1 -1
- package/dist/harbor/core.d.ts +2 -0
- package/dist/harbor/core.js +114 -25
- package/dist/harbor/dial.js +5 -3
- package/dist/harbor/memory.d.ts +1 -1
- package/dist/harbor/memory.js +3 -2
- package/dist/harbor/reach.js +32 -7
- package/dist/ward/allowance.js +8 -2
- package/dist/ward/arithmetic.js +11 -3
- package/dist/ward/cells.d.ts +3 -1
- package/dist/ward/cells.js +79 -21
- package/dist/ward/door.d.ts +1 -0
- package/dist/ward/door.js +24 -5
- package/dist/ward/ground.d.ts +1 -0
- package/dist/ward/heirs.d.ts +1 -1
- package/dist/ward/heirs.js +15 -8
- package/dist/ward/owner.d.ts +5 -26
- package/dist/ward/owner.js +24 -15
- package/dist/ward/seal.js +3 -0
- package/dist/ward/stance.d.ts +4 -1
- package/dist/ward/stance.js +116 -54
- package/dist/ward/ward.d.ts +10 -0
- package/dist/ward/ward.js +87 -36
- package/package.json +4 -2
- package/src/being/being.ts +26 -10
- package/src/being/digest.ts +13 -11
- package/src/being/types.ts +10 -4
- package/src/conformance/estate.ts +1 -3
- package/src/conformance/index.ts +1 -1
- package/src/harbor/core.ts +110 -26
- package/src/harbor/dial.ts +6 -4
- package/src/harbor/memory.ts +3 -2
- package/src/harbor/reach.ts +38 -11
- package/src/ward/allowance.ts +8 -2
- package/src/ward/arithmetic.ts +11 -3
- package/src/ward/cells.ts +76 -25
- package/src/ward/door.ts +22 -5
- package/src/ward/ground.ts +9 -2
- package/src/ward/heirs.ts +13 -6
- package/src/ward/owner.ts +24 -24
- package/src/ward/seal.ts +2 -0
- package/src/ward/stance.ts +117 -48
- package/src/ward/ward.ts +86 -35
- package/vectors/arithmetic.json +7 -0
- package/vectors/framing.json +14 -5
package/src/ward/cells.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// persist a being's cells as it likes: as this process's objects, as a row, as
|
|
4
4
|
// a line of JSON on a disk. That promise is only worth what the cells honour.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// go and come back from. Through any harbor that keeps values it
|
|
9
|
-
//
|
|
10
|
-
// harbor she is standing in. That
|
|
11
|
-
// the one thing it must never do.
|
|
6
|
+
// Unchecked, a being could write a Map, a Date, an undefined, a NaN. In a
|
|
7
|
+
// memory harbor the object would survive, because there is nowhere for it to
|
|
8
|
+
// go and come back from. Through any harbor that keeps values it would
|
|
9
|
+
// silently become {}, or a string, or absent, or null, and she is never told
|
|
10
|
+
// which harbor she is standing in. That would be the memory harbor lying to
|
|
11
|
+
// her, which is the one thing it must never do.
|
|
12
12
|
//
|
|
13
13
|
// So the cells refuse the write. Loud, where she wrote it, in her own frame:
|
|
14
14
|
// a being holds three answers and a throw is not one of them, so this throw
|
|
@@ -17,18 +17,27 @@
|
|
|
17
17
|
// that a harbor would have to lie about later.
|
|
18
18
|
import type { Cells, Json } from '../being/types.ts';
|
|
19
19
|
|
|
20
|
+
// How deep a value may nest. Every walk over a value in the kit recurses,
|
|
21
|
+
// and a peer chooses the depth of what she answers; a bound keeps a hostile
|
|
22
|
+
// reply from ending a walk in a stack overflow instead of a refusal.
|
|
23
|
+
export const DEPTH = 64;
|
|
24
|
+
|
|
20
25
|
// I-JSON, all the way down. A number that JSON cannot write is not a number
|
|
21
|
-
// a harbor can keep,
|
|
22
|
-
|
|
26
|
+
// a harbor can keep, a key on the prototype is not a key she wrote, and a
|
|
27
|
+
// hole in a list, or a key named `__proto__`, is a thing JSON writes one way
|
|
28
|
+
// and a runtime reads another.
|
|
29
|
+
function fault(v: unknown, path: string, seen: Set<object>, depth: number): string | null {
|
|
23
30
|
if (v === null || typeof v === 'boolean' || typeof v === 'string') return null;
|
|
24
31
|
if (typeof v === 'number') return Number.isFinite(v) ? null : `${path} is ${String(v)}, which no harbor can write down`;
|
|
25
32
|
if (typeof v !== 'object') return `${path} is a ${typeof v}, which is not a value`;
|
|
26
33
|
if (seen.has(v)) return `${path} refers back to itself`;
|
|
34
|
+
if (depth >= DEPTH) return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
|
|
27
35
|
seen.add(v);
|
|
28
36
|
try {
|
|
29
37
|
if (Array.isArray(v)) {
|
|
30
38
|
for (let i = 0; i < v.length; i += 1) {
|
|
31
|
-
|
|
39
|
+
if (!(i in v)) return `${path}[${i}] is a hole, which no harbor can write down`;
|
|
40
|
+
const f = fault(v[i], `${path}[${i}]`, seen, depth + 1);
|
|
32
41
|
if (f) return f;
|
|
33
42
|
}
|
|
34
43
|
return null;
|
|
@@ -36,7 +45,8 @@ function fault(v: unknown, path: string, seen: Set<object>): string | null {
|
|
|
36
45
|
const proto = Object.getPrototypeOf(v);
|
|
37
46
|
if (proto !== Object.prototype && proto !== null) return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
|
|
38
47
|
for (const k of Object.keys(v)) {
|
|
39
|
-
|
|
48
|
+
if (k === '__proto__') return `${path}.__proto__ is a key no harbor can keep`;
|
|
49
|
+
const f = fault((v as Record<string, unknown>)[k], `${path}.${k}`, seen, depth + 1);
|
|
40
50
|
if (f) return f;
|
|
41
51
|
}
|
|
42
52
|
return null;
|
|
@@ -45,16 +55,43 @@ function fault(v: unknown, path: string, seen: Set<object>): string | null {
|
|
|
45
55
|
}
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
export const cellFault = (v: unknown, path: string): string | null => fault(v, path, new Set());
|
|
58
|
+
export const cellFault = (v: unknown, path: string): string | null => fault(v, path, new Set(), 0);
|
|
59
|
+
|
|
60
|
+
// The three keys at the root of her cells that are the ward's: it writes
|
|
61
|
+
// them, she reads them, and a write of hers there is refused like a non-value.
|
|
62
|
+
const WARDS = new Set(['standings', 'occupants', 'class']);
|
|
49
63
|
|
|
50
64
|
// The guard is one proxy at the root and one for every container read through
|
|
51
65
|
// it, so a write nested three deep is refused the same way a write at the top
|
|
52
66
|
// is. Wrappers are remembered, so reading the same array twice is the same
|
|
53
67
|
// object twice and a being may still compare what she holds.
|
|
54
68
|
const wrapped = new WeakMap<object, object>();
|
|
69
|
+
const targets = new WeakMap<object, object>();
|
|
55
70
|
const guards = new WeakSet();
|
|
56
71
|
|
|
57
|
-
|
|
72
|
+
// A key a write may land on: a string that is not `__proto__`, and not one
|
|
73
|
+
// of the ward's at the root. A symbol key is a thing JSON never writes.
|
|
74
|
+
const refuse: (why: string) => never = (why) => {
|
|
75
|
+
throw new TypeError(`cells hold values: ${why}`);
|
|
76
|
+
};
|
|
77
|
+
function keyFault(t: object, k: string | symbol, v: unknown, path: string, root: boolean): void {
|
|
78
|
+
if (typeof k !== 'string') refuse(`${path} takes no symbol key`);
|
|
79
|
+
if (k === '__proto__') refuse(`${path}.__proto__ is a key no harbor can keep`);
|
|
80
|
+
if (root && WARDS.has(k)) refuse(`${path}.${k} is the ward's to write`);
|
|
81
|
+
// A list grows by one at its end, or it has holes JSON cannot write. A
|
|
82
|
+
// push sets the slot at its length and then the length: both pass. A
|
|
83
|
+
// length set past what she wrote, or a slot beyond it, would leave holes.
|
|
84
|
+
if (Array.isArray(t)) {
|
|
85
|
+
if (k === 'length') {
|
|
86
|
+
if (typeof v !== 'number' || v > t.length) refuse(`${path}.length set past what she wrote would leave holes`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const i = Number(k);
|
|
90
|
+
if (Number.isInteger(i) && i > t.length) refuse(`${path}[${i}] would leave a hole`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function guard<T extends object>(target: T, path: string, wrote: () => void, root = false): T {
|
|
58
95
|
if (guards.has(target)) return target;
|
|
59
96
|
const had = wrapped.get(target);
|
|
60
97
|
if (had) return had as T;
|
|
@@ -62,29 +99,43 @@ function guard<T extends object>(target: T, path: string): T {
|
|
|
62
99
|
get(t, k, r) {
|
|
63
100
|
const v = Reflect.get(t, k, r);
|
|
64
101
|
// A container reached through her cells is part of her cells.
|
|
65
|
-
return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v as object, `${path}.${String(k)}
|
|
102
|
+
return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v as object, `${path}.${String(k)}`, wrote) : v;
|
|
66
103
|
},
|
|
67
104
|
set(t, k, v, r) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
keyFault(t, k, v, path, root);
|
|
106
|
+
const f = cellFault(v, `${path}.${String(k)}`);
|
|
107
|
+
if (f) refuse(f);
|
|
108
|
+
const ok = Reflect.set(t, k, v, r);
|
|
109
|
+
wrote();
|
|
110
|
+
return ok;
|
|
73
111
|
},
|
|
74
112
|
defineProperty(t, k, d) {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
113
|
+
if (!('value' in d)) refuse(`${path}.${String(k)} is an accessor, which no harbor can keep`);
|
|
114
|
+
keyFault(t, k, d.value, path, root);
|
|
115
|
+
const f = cellFault(d.value, `${path}.${String(k)}`);
|
|
116
|
+
if (f) refuse(f);
|
|
117
|
+
const ok = Reflect.defineProperty(t, k, d);
|
|
118
|
+
wrote();
|
|
119
|
+
return ok;
|
|
120
|
+
},
|
|
121
|
+
deleteProperty(t, k) {
|
|
122
|
+
if (root && typeof k === 'string' && WARDS.has(k)) refuse(`${path}.${k} is the ward's to keep`);
|
|
123
|
+
const ok = Reflect.deleteProperty(t, k);
|
|
124
|
+
wrote();
|
|
125
|
+
return ok;
|
|
80
126
|
},
|
|
81
127
|
});
|
|
82
128
|
wrapped.set(target, p);
|
|
129
|
+
targets.set(p, target);
|
|
83
130
|
guards.add(p);
|
|
84
131
|
return p;
|
|
85
132
|
}
|
|
86
133
|
|
|
87
134
|
// Her cells, guarded. Called once per being at boot; the guarded object is
|
|
88
135
|
// what goes into the partition and what the stance hands her, so there is no
|
|
89
|
-
// second door onto the same cells.
|
|
90
|
-
|
|
136
|
+
// second door onto the same cells. Every write that passes says so to the
|
|
137
|
+
// ward, which says so to its harbor.
|
|
138
|
+
export const guardCells = (cells: Cells, wrote: () => void = () => {}): Cells => guard(cells as unknown as Record<string, Json>, 'cells', wrote, true) as unknown as Cells;
|
|
139
|
+
// The cells behind the guard, for the ward alone: the one writer of the
|
|
140
|
+
// three keys that are its own.
|
|
141
|
+
export const unguarded = (cells: Cells): Cells => (targets.get(cells as unknown as object) ?? cells) as Cells;
|
package/src/ward/door.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { Heirs } from './heirs.ts';
|
|
|
15
15
|
import { openAsk, sealReply, verifyAsk, type ReplyPayload, type WardKey } from './seal.ts';
|
|
16
16
|
import { spent } from './allowance.ts';
|
|
17
17
|
import { KEY, sealingPair } from './arithmetic.ts';
|
|
18
|
+
import { cellFault } from './cells.ts';
|
|
18
19
|
|
|
19
20
|
export type Door = { key: string; being: BeingLike; cells: { occupants: Record<string, unknown> } };
|
|
20
21
|
export type Judged = { bytes: Uint8Array; heard: boolean };
|
|
@@ -37,13 +38,21 @@ export async function arrive(door: Door, asker: Asker, method: string | undefine
|
|
|
37
38
|
// on the way out and the far side reads back as a fourth word.
|
|
38
39
|
if (out === undefined || isSilence(out)) return SILENCE;
|
|
39
40
|
if (isWord(out)) return threw; // a word is the ward's to say, never hers
|
|
41
|
+
// Her answer is held to the rule her args and her cells are held to. A
|
|
42
|
+
// shape JSON would drop or rewrite on the way out, a Date, a Map, a NaN, a
|
|
43
|
+
// cycle, is not hers to make: the far side would read something she never
|
|
44
|
+
// said, or the door itself would fail to write her reply after the number
|
|
45
|
+
// was spent. It is threw, like a word out of her.
|
|
46
|
+
if (cellFault(out, 'answer') !== null) return threw;
|
|
40
47
|
if (method === undefined) return { object: out, seen: null };
|
|
41
48
|
// The digest rides along, it is not the answer. She has already answered:
|
|
42
49
|
// a describe that will not run costs the digest, and nothing else.
|
|
43
50
|
return { object: out, seen: await seen(door, asker) };
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
// Her digest for this asker, or null when her describe threw or fell silent.
|
|
54
|
+
// The owner's describe reads it the same way for every being of the ward.
|
|
55
|
+
export async function seen(door: Door, asker: Asker): Promise<string | null> {
|
|
47
56
|
try {
|
|
48
57
|
const bp = await door.being.answer(asker);
|
|
49
58
|
return isSilence(bp) || isWord(bp) ? null : await digest(bp);
|
|
@@ -83,6 +92,12 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
|
|
|
83
92
|
return { reply: said('removed'), ephemeralPk, heard: true };
|
|
84
93
|
}
|
|
85
94
|
if (!(await verifyAsk(a, payload.by))) return refuse(); // D7, under an admitted key
|
|
95
|
+
// The signature took time, and another arrival on this heir may have been
|
|
96
|
+
// honoured meanwhile: a knock that raced this one and won spent the heir,
|
|
97
|
+
// and the key this ask speaks under may not be admitted any more. The
|
|
98
|
+
// door judges concurrently, so admission is read again now that writing
|
|
99
|
+
// is next, and what changed under the await is judged as it stands.
|
|
100
|
+
if (!heirs.admits(to, payload.by)) return heirs.gone(to, payload.by) ? { reply: said('removed'), ephemeralPk, heard: true } : refuse();
|
|
86
101
|
// From here the door has heard a key it holds. Every answer below is
|
|
87
102
|
// sealed to that key's lid; nothing below is a stranger's.
|
|
88
103
|
const door = doors.get(h.being);
|
|
@@ -100,9 +115,11 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
|
|
|
100
115
|
// reply is sealed to the ephemeral pk on its lid, which is all a stranger
|
|
101
116
|
// holds, and says silence.
|
|
102
117
|
//
|
|
103
|
-
// A lid that is not a key
|
|
104
|
-
// have several each
|
|
105
|
-
// door never throws: that reply is noise, sealed to a key
|
|
118
|
+
// A lid that is not a key, a small-order point, of which the two curves
|
|
119
|
+
// have several each, makes a dead agreement, and the seal refuses it. The
|
|
120
|
+
// door never throws: that reply is noise, a plain silence sealed to a key
|
|
121
|
+
// nobody holds, and the same is written for anything else the seal will
|
|
122
|
+
// not take.
|
|
106
123
|
return async function door(bytes: Uint8Array): Promise<Judged> {
|
|
107
124
|
const out = await judge(bytes);
|
|
108
125
|
const reply = out?.reply ?? SILENCE;
|
|
@@ -110,7 +127,7 @@ export function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, p
|
|
|
110
127
|
try {
|
|
111
128
|
return { bytes: await sealReply(reply, out?.ephemeralPk ?? lid(bytes, random), key.sign, random(32)), heard };
|
|
112
129
|
} catch {
|
|
113
|
-
return { bytes: await sealReply(
|
|
130
|
+
return { bytes: await sealReply(SILENCE, (await sealingPair(random(32))).pk, key.sign, random(32)), heard };
|
|
114
131
|
}
|
|
115
132
|
};
|
|
116
133
|
}
|
package/src/ward/ground.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// The ground. The one object a harbor passes a ward at birth.
|
|
3
|
-
// never a
|
|
2
|
+
// The ground. The one object a harbor passes a ward at birth. Six things,
|
|
3
|
+
// never a seventh. Everything a runtime differs on arrives here, which is
|
|
4
4
|
// why the ward itself knows no runtime.
|
|
5
5
|
import type { Stance, BeingLike } from '../being/types.ts';
|
|
6
6
|
|
|
@@ -8,6 +8,13 @@ export type Ground = {
|
|
|
8
8
|
seed: string | Uint8Array; // the ward derives its pk from it and nothing else
|
|
9
9
|
memory: Record<string, unknown>; // the partition. the ward's files. opaque to the harbor
|
|
10
10
|
instantiate(className: string, stance: Stance): BeingLike | null; // the code half
|
|
11
|
+
// The ward wrote its partition. It says so after every write, a key
|
|
12
|
+
// rotated, a relation taken, a cell she set, and says nothing else: it
|
|
13
|
+
// never learns whether anything was kept. A harbor that keeps the
|
|
14
|
+
// partition saves after this, when it likes and in the order it was told,
|
|
15
|
+
// so a being driven in process is kept the way one reached through a door
|
|
16
|
+
// is. A harbor that keeps nothing leaves it out.
|
|
17
|
+
wrote?: () => void;
|
|
11
18
|
// Sealed bytes to a ward pk. What comes back, or undefined.
|
|
12
19
|
//
|
|
13
20
|
// undefined is a promise, not a shrug: no door was reached, and nothing was
|
package/src/ward/heirs.ts
CHANGED
|
@@ -13,11 +13,14 @@ const SPAN = 64;
|
|
|
13
13
|
|
|
14
14
|
export class Heirs {
|
|
15
15
|
#p: Partition;
|
|
16
|
-
|
|
16
|
+
#wrote: () => void;
|
|
17
|
+
constructor(p: Partition, wrote: () => void = () => {}) {
|
|
17
18
|
this.#p = p;
|
|
19
|
+
this.#wrote = wrote;
|
|
18
20
|
}
|
|
19
21
|
open(heir: string, being: string, id: string): void {
|
|
20
22
|
this.#p.heirs[heir] = { being, id, current: heir, announced: null, fresh: true, mark: 0, spent: [] };
|
|
23
|
+
this.#wrote();
|
|
21
24
|
}
|
|
22
25
|
// The id was removed. The heir is forgotten, and its last keys are kept
|
|
23
26
|
// apart, bounded, so that whoever still holds them hears `removed` at the
|
|
@@ -29,6 +32,7 @@ export class Heirs {
|
|
|
29
32
|
this.#p.gone[heir] = { current: h.current, announced: h.announced };
|
|
30
33
|
const keys = Object.keys(this.#p.gone);
|
|
31
34
|
for (const old of keys.slice(0, Math.max(0, keys.length - GONE))) delete this.#p.gone[old];
|
|
35
|
+
this.#wrote();
|
|
32
36
|
}
|
|
33
37
|
// May `by` speak for a relation she removed? True only for the keys the
|
|
34
38
|
// door held when the id went, which nobody but their holder has.
|
|
@@ -79,17 +83,20 @@ export class Heirs {
|
|
|
79
83
|
// being refused, or a stranger who cannot be heard would still leave a mark
|
|
80
84
|
// behind her. Every write below this line is one that is going to hold.
|
|
81
85
|
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
|
|
86
|
+
if (h.fresh && (next === null || next === h.current)) return 'unannounced'; // a knock without a key of her own binds nothing, and the heir is not a key of her own
|
|
83
87
|
if (!this.spend(h, seq)) return 'repeated';
|
|
84
|
-
|
|
88
|
+
const settled = this.settle(h, by, next);
|
|
89
|
+
this.#wrote(); // the number is spent whether or not the keys settled
|
|
90
|
+
return settled ? true : 'unannounced';
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
// The signature checked out. Settle the keys: a fresh heir rotates at once
|
|
88
|
-
// to what it announced and must announce something
|
|
89
|
-
//
|
|
94
|
+
// to what it announced and must announce something that is not itself, so
|
|
95
|
+
// that the heir dies as it speaks; a current key replaces its
|
|
96
|
+
// announcement; an announced key becomes current.
|
|
90
97
|
settle(h: Heir, by: string, next: string | null): boolean {
|
|
91
98
|
if (h.fresh) {
|
|
92
|
-
if (next === null) return false;
|
|
99
|
+
if (next === null || next === h.current) return false;
|
|
93
100
|
h.current = next;
|
|
94
101
|
h.announced = null;
|
|
95
102
|
h.fresh = false;
|
package/src/ward/owner.ts
CHANGED
|
@@ -9,23 +9,18 @@
|
|
|
9
9
|
// her cells under the id the owner gave; remove takes a relation out of her
|
|
10
10
|
// by id, the mirror of it.
|
|
11
11
|
import { isSilence, isWord, wordOf } from '../being/silence.ts';
|
|
12
|
-
import { digest } from '../being/digest.ts';
|
|
13
12
|
import { OWNER } from '../being/types.ts';
|
|
14
13
|
import type { Ask, Asker, Invitation, Json, JsonObject, Wanted } from '../being/types.ts';
|
|
14
|
+
import { put } from './partition.ts';
|
|
15
|
+
import { seen } from './door.ts';
|
|
16
|
+
import type { Booted } from './ward.ts';
|
|
15
17
|
|
|
16
18
|
export type OwnerSide = {
|
|
17
19
|
pk: string;
|
|
18
|
-
doors: Map<
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
key: string;
|
|
22
|
-
being: { answer: (...a: never[]) => unknown };
|
|
23
|
-
cells: { class?: string; occupants: Record<string, unknown>; standings: Record<string, unknown> };
|
|
24
|
-
stance: { occupants: { invite(id: string): Promise<Invitation | null>; remove(id: string): void }; standings: { knock(inv: Invitation, m?: string, a?: JsonObject, w?: Wanted): Promise<unknown>; take(id: string, inv: Invitation): Promise<string | null>; remove(id: string): void } };
|
|
25
|
-
}
|
|
26
|
-
>;
|
|
20
|
+
doors: Map<string, Booted>;
|
|
21
|
+
absent(): Record<string, string | null>; // the rows no door holds this run: key to class
|
|
27
22
|
publicKey(): string | null;
|
|
28
|
-
instantiate(key: string, className: string):
|
|
23
|
+
instantiate(key: string, className: string): Booted | 'threw' | null;
|
|
29
24
|
unboot(key: string): string[] | null;
|
|
30
25
|
setPublic(key: string): void;
|
|
31
26
|
};
|
|
@@ -34,7 +29,7 @@ const str = { type: 'string' };
|
|
|
34
29
|
export const OWNER_ASKS: Ask[] = [
|
|
35
30
|
{ name: 'boot', description: 'boot a being by class name, under a key the owner chooses', input: { type: 'object', properties: { key: str, class: str }, required: ['key', 'class'] } },
|
|
36
31
|
{ name: 'public', description: 'mark a booted being as the one public being of the ward, reached by anyone at the bare pk', input: { type: 'object', properties: { key: str }, required: ['key'] } },
|
|
37
|
-
{ name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str }, required: ['being', 'id'] } },
|
|
32
|
+
{ name: 'invite', description: 'mint an invitation on a being of the ward, under the id she will know the occupant by, with the notes she will read on it; on the ward pk it mints an owner, and only the root may', input: { type: 'object', properties: { being: str, id: str, notes: { type: 'object' } }, required: ['being', 'id'] } },
|
|
38
33
|
{
|
|
39
34
|
name: 'knock',
|
|
40
35
|
description: 'knock for a being of the ward with an invitation, and take the standing under id if answered; being is a key booted, or { boot: class, key } to boot her first',
|
|
@@ -56,15 +51,18 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
56
51
|
className = word(args.class);
|
|
57
52
|
if (key === null || className === null) return { error: 'no such class, or key taken' };
|
|
58
53
|
const door = w.instantiate(key, className);
|
|
54
|
+
if (door === 'threw') return { error: 'threw at birth' };
|
|
59
55
|
return door ? { booted: door.key } : { error: 'no such class, or key taken' };
|
|
60
56
|
}
|
|
61
57
|
if (method === 'public') {
|
|
62
58
|
// A ward may have one public being, and no more. Marking a second would
|
|
63
59
|
// leave the first holding every relation she had, reachable by nobody at
|
|
64
|
-
// the bare pk, and told by nobody that she had been replaced.
|
|
60
|
+
// the bare pk, and told by nobody that she had been replaced. One absent
|
|
61
|
+
// this run is reachable by nobody already, so the mark may move off her.
|
|
65
62
|
const key = word(args.key);
|
|
66
63
|
if (key === null || key === w.pk || !w.doors.has(key)) return { error: 'no such being' };
|
|
67
|
-
|
|
64
|
+
const standing = w.publicKey();
|
|
65
|
+
if (standing !== null && standing !== key && w.doors.has(standing)) return { error: 'a ward has one public being' };
|
|
68
66
|
w.setPublic(key);
|
|
69
67
|
return { public: key };
|
|
70
68
|
}
|
|
@@ -77,8 +75,13 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
77
75
|
id = word(args.id);
|
|
78
76
|
if (being === null || id === null) return { error: 'no such being' };
|
|
79
77
|
if (being === w.pk && asker.id !== OWNER) return { error: 'no such being' };
|
|
78
|
+
// The notes are the terms the owner mints under, and the being reads them
|
|
79
|
+
// on her gate. Placing an occupant is the owner's, and saying what it may
|
|
80
|
+
// do is part of placing it; the work behind the gate is still hers alone.
|
|
81
|
+
const notes = args.notes;
|
|
80
82
|
const door = w.doors.get(being);
|
|
81
|
-
|
|
83
|
+
if (!door) return { error: 'no such being' };
|
|
84
|
+
return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? (notes as JsonObject) : undefined);
|
|
82
85
|
}
|
|
83
86
|
if (method === 'knock') {
|
|
84
87
|
const b = args.being as string | { boot: unknown; key: unknown };
|
|
@@ -90,6 +93,7 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
90
93
|
className = word(b.boot);
|
|
91
94
|
if (key === null || className === null) return { error: 'no such being' };
|
|
92
95
|
const made = w.instantiate(key, className);
|
|
96
|
+
if (made === 'threw') return { error: 'threw at birth' };
|
|
93
97
|
door = made ? w.doors.get(made.key) : w.doors.get(key);
|
|
94
98
|
}
|
|
95
99
|
const id = word(args.id);
|
|
@@ -139,20 +143,16 @@ export async function ownerAnswer(w: OwnerSide, asker: Asker, method: string | u
|
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
// The ward's describe for its owner: its beings, their classes, a digest each,
|
|
142
|
-
// as she would describe herself to the owner.
|
|
146
|
+
// as she would describe herself to the owner. A being absent this run is
|
|
147
|
+
// listed too, with a null digest and absent true, so the owner can see her
|
|
148
|
+
// and take her out.
|
|
143
149
|
async function describe(w: OwnerSide): Promise<Json> {
|
|
144
150
|
const beings: Record<string, Json> = {};
|
|
145
151
|
for (const [key, door] of w.doors) {
|
|
146
152
|
if (key === w.pk) continue;
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const bp = await (door.being.answer as (a: unknown) => unknown)({ id: OWNER });
|
|
150
|
-
d = isSilence(bp) || isWord(bp) ? null : await digest(bp as Json);
|
|
151
|
-
} catch {
|
|
152
|
-
// she threw where she was asked. there is no blueprint, and d stays null.
|
|
153
|
-
}
|
|
154
|
-
beings[key] = { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d };
|
|
153
|
+
put(beings, key, { class: door.cells.class ?? null, public: w.publicKey() === key, digest: await seen(door, { id: OWNER }) });
|
|
155
154
|
}
|
|
155
|
+
for (const [key, cls] of Object.entries(w.absent())) put(beings, key, { class: cls, public: w.publicKey() === key, digest: null, absent: true });
|
|
156
156
|
return {
|
|
157
157
|
asks: OWNER_ASKS,
|
|
158
158
|
notes: { pk: w.pk, beings },
|
package/src/ward/seal.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import type { DoorWord, Json, JsonObject } from '../being/types.ts';
|
|
23
23
|
import { isDoorWord } from '../being/silence.ts';
|
|
24
24
|
import { KEY, SIGNATURE, box, concat, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.ts';
|
|
25
|
+
import { cellFault } from './cells.ts';
|
|
25
26
|
|
|
26
27
|
const utf8 = new TextEncoder();
|
|
27
28
|
const text = new TextDecoder();
|
|
@@ -87,6 +88,7 @@ export async function openAsk(bytes: Uint8Array, padlockSecret: Uint8Array): Pro
|
|
|
87
88
|
if (payload.next !== null && !(typeof payload.next === 'string' && /^[0-9a-f]{64}$/.test(payload.next))) return null;
|
|
88
89
|
if (payload.method !== undefined && typeof payload.method !== 'string') return null; // a name, or the empty ask. never a number, never an object.
|
|
89
90
|
if (payload.args !== undefined && (payload.args === null || typeof payload.args !== 'object' || Array.isArray(payload.args))) return null; // args are one object, or absent. a string or a list is not an ask.
|
|
91
|
+
if (payload.args !== undefined && cellFault(payload.args, 'args') !== null) return null; // and values all the way down: no key named __proto__, no nesting past the bound
|
|
90
92
|
if (!Number.isSafeInteger(payload.seq) || payload.seq < 1) return null; // her count for this relation. one and up, and a whole number.
|
|
91
93
|
// The allowance. A whole number, and one that has already run out is not an
|
|
92
94
|
// ask this door will open: it is refused as the bytes it is, above.
|