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