@quo-systems/quo 0.2.9 → 0.2.11
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 +301 -110
- package/dist/being/being.d.ts +2 -2
- package/dist/being/being.js +34 -12
- package/dist/being/digest.js +26 -8
- package/dist/being/index.d.ts +2 -2
- package/dist/being/index.js +2 -2
- package/dist/being/silence.d.ts +2 -0
- package/dist/being/silence.js +12 -0
- package/dist/being/types.d.ts +4 -2
- package/dist/being/types.js +25 -0
- package/dist/conformance/assert.js +40 -6
- package/dist/conformance/beings.d.ts +48 -5
- package/dist/conformance/beings.js +39 -8
- package/dist/conformance/estate.js +110 -21
- package/dist/conformance/index.d.ts +5 -2
- package/dist/conformance/index.js +165 -7
- package/dist/harbor/core.d.ts +5 -2
- package/dist/harbor/core.js +195 -45
- package/dist/harbor/dial.js +32 -15
- package/dist/harbor/index.d.ts +1 -0
- package/dist/harbor/index.js +3 -0
- package/dist/harbor/memory.d.ts +3 -3
- package/dist/harbor/memory.js +7 -12
- package/dist/harbor/reach.js +42 -17
- package/dist/ward/allowance.js +15 -4
- package/dist/ward/arithmetic.d.ts +1 -0
- package/dist/ward/arithmetic.js +22 -6
- package/dist/ward/cells.d.ts +3 -1
- package/dist/ward/cells.js +79 -21
- package/dist/ward/door.d.ts +3 -2
- package/dist/ward/door.js +38 -10
- package/dist/ward/ground.d.ts +5 -1
- package/dist/ward/ground.js +38 -1
- package/dist/ward/heirs.d.ts +2 -3
- package/dist/ward/heirs.js +19 -13
- package/dist/ward/index.d.ts +1 -0
- package/dist/ward/index.js +3 -0
- package/dist/ward/owner.d.ts +6 -27
- package/dist/ward/owner.js +59 -33
- package/dist/ward/partition.d.ts +3 -0
- package/dist/ward/partition.js +109 -4
- package/dist/ward/seal.d.ts +1 -0
- package/dist/ward/seal.js +41 -13
- package/dist/ward/stance.d.ts +7 -3
- package/dist/ward/stance.js +156 -66
- package/dist/ward/ward.d.ts +10 -0
- package/dist/ward/ward.js +123 -51
- package/package.json +4 -2
- package/src/being/being.ts +33 -11
- package/src/being/digest.ts +28 -13
- package/src/being/index.ts +2 -2
- package/src/being/silence.ts +14 -0
- package/src/being/types.ts +39 -5
- package/src/conformance/assert.ts +37 -4
- package/src/conformance/beings.ts +41 -10
- package/src/conformance/estate.ts +107 -20
- package/src/conformance/index.ts +188 -13
- package/src/harbor/core.ts +203 -46
- package/src/harbor/dial.ts +46 -17
- package/src/harbor/index.ts +3 -0
- package/src/harbor/memory.ts +8 -13
- package/src/harbor/reach.ts +47 -21
- package/src/ward/allowance.ts +15 -4
- package/src/ward/arithmetic.ts +25 -8
- package/src/ward/cells.ts +76 -25
- package/src/ward/door.ts +38 -12
- package/src/ward/ground.ts +52 -3
- package/src/ward/heirs.ts +19 -13
- package/src/ward/index.ts +3 -0
- package/src/ward/owner.ts +65 -46
- package/src/ward/partition.ts +109 -5
- package/src/ward/seal.ts +41 -12
- package/src/ward/stance.ts +163 -64
- package/src/ward/ward.ts +124 -52
- package/vectors/arithmetic.json +7 -0
- package/vectors/framing.json +30 -15
- package/vectors/wire.json +4 -4
package/dist/ward/owner.js
CHANGED
|
@@ -9,13 +9,15 @@
|
|
|
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';
|
|
15
|
+
import { within, DEFAULT, LATE } from './allowance.js';
|
|
14
16
|
const str = { type: 'string' };
|
|
15
17
|
export const OWNER_ASKS = [
|
|
16
18
|
{ 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
|
-
{ 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: 'public', description: 'mark a booted being as the one public being of the ward, reached by anyone at the bare pk; null takes the mark off and leaves her booted', input: { type: 'object', properties: { key: str }, required: ['key'] } },
|
|
20
|
+
{ 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
21
|
{
|
|
20
22
|
name: 'knock',
|
|
21
23
|
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',
|
|
@@ -25,27 +27,40 @@ export const OWNER_ASKS = [
|
|
|
25
27
|
{ name: 'unboot', description: 'take a being out of the ward, with every relation she holds; her occupants hear removed, and the ward itself is refused', input: { type: 'object', properties: { being: str }, required: ['being'] } },
|
|
26
28
|
];
|
|
27
29
|
export async function ownerAnswer(w, asker, method, args) {
|
|
28
|
-
// Every name the owner gives is a
|
|
29
|
-
// object passed where a name belongs becomes the string
|
|
30
|
-
// and two owners who each got the shape wrong would be
|
|
31
|
-
|
|
30
|
+
// Every name the owner gives is a string, or it is nothing. Coerced instead
|
|
31
|
+
// of checked, an object passed where a name belongs becomes the string
|
|
32
|
+
// "[object Object]", and two owners who each got the shape wrong would be
|
|
33
|
+
// handed one being. Not `word`: a word in this tree is what a ward says when
|
|
34
|
+
// no object came back, and this file imports those.
|
|
35
|
+
const named = (v) => (typeof v === 'string' ? v : null);
|
|
32
36
|
if (method === undefined)
|
|
33
37
|
return describe(w);
|
|
34
38
|
if (method === 'boot') {
|
|
35
|
-
const key =
|
|
39
|
+
const key = named(args.key), className = named(args.class);
|
|
36
40
|
if (key === null || className === null)
|
|
37
41
|
return { error: 'no such class, or key taken' };
|
|
38
|
-
const door = w.instantiate(
|
|
42
|
+
const door = w.instantiate(className, key);
|
|
43
|
+
if (door === 'threw')
|
|
44
|
+
return { error: 'threw at birth' };
|
|
39
45
|
return door ? { booted: door.key } : { error: 'no such class, or key taken' };
|
|
40
46
|
}
|
|
41
47
|
if (method === 'public') {
|
|
42
48
|
// A ward may have one public being, and no more. Marking a second would
|
|
43
49
|
// 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.
|
|
45
|
-
|
|
50
|
+
// the bare pk, and told by nobody that she had been replaced. One absent
|
|
51
|
+
// this run is reachable by nobody already, so the mark may move off her.
|
|
52
|
+
// Null takes the mark off: the being stays and keeps every relation she
|
|
53
|
+
// holds, and the ward answers arrivals for no heir with silence, as one
|
|
54
|
+
// that never had a public being does. Retiring her is not destroying her.
|
|
55
|
+
if (args.key === null) {
|
|
56
|
+
w.setPublic(null);
|
|
57
|
+
return { public: null };
|
|
58
|
+
}
|
|
59
|
+
const key = named(args.key);
|
|
46
60
|
if (key === null || key === w.pk || !w.doors.has(key))
|
|
47
61
|
return { error: 'no such being' };
|
|
48
|
-
|
|
62
|
+
const standing = w.publicKey();
|
|
63
|
+
if (standing !== null && standing !== key && w.doors.has(standing))
|
|
49
64
|
return { error: 'a ward has one public being' };
|
|
50
65
|
w.setPublic(key);
|
|
51
66
|
return { public: key };
|
|
@@ -55,13 +70,19 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
55
70
|
// is an owner, and ownership is minted by the ask pointer alone. An owner
|
|
56
71
|
// at the door asking to invite on the ward is refused, so that a carried
|
|
57
72
|
// key can pilot but never hand piloting on.
|
|
58
|
-
const being =
|
|
73
|
+
const being = named(args.being), id = named(args.id);
|
|
59
74
|
if (being === null || id === null)
|
|
60
75
|
return { error: 'no such being' };
|
|
61
76
|
if (being === w.pk && asker.id !== OWNER)
|
|
62
77
|
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;
|
|
63
82
|
const door = w.doors.get(being);
|
|
64
|
-
|
|
83
|
+
if (!door)
|
|
84
|
+
return { error: 'no such being' };
|
|
85
|
+
return await door.stance.occupants.invite(id, notes !== null && typeof notes === 'object' && !Array.isArray(notes) ? notes : undefined);
|
|
65
86
|
}
|
|
66
87
|
if (method === 'knock') {
|
|
67
88
|
const b = args.being;
|
|
@@ -69,13 +90,17 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
69
90
|
if (!door && b && typeof b === 'object') {
|
|
70
91
|
// new or existing: the owner names a being of theirs, and a key already
|
|
71
92
|
// booted is a being of theirs, not a class that failed to instantiate.
|
|
72
|
-
const key =
|
|
93
|
+
const key = named(b.key), className = named(b.boot);
|
|
73
94
|
if (key === null || className === null)
|
|
74
95
|
return { error: 'no such being' };
|
|
75
|
-
const made = w.instantiate(
|
|
76
|
-
|
|
96
|
+
const made = w.instantiate(className, key);
|
|
97
|
+
if (made === 'threw')
|
|
98
|
+
return { error: 'threw at birth' };
|
|
99
|
+
// Made or already there, the door is the one under the key she named:
|
|
100
|
+
// instantiate boots under that key or refuses, and never another.
|
|
101
|
+
door = w.doors.get(key);
|
|
77
102
|
}
|
|
78
|
-
const id =
|
|
103
|
+
const id = named(args.id);
|
|
79
104
|
// Never for the ward itself, for the reason invite refuses it: the ward
|
|
80
105
|
// is a being to nobody outside, and holds no relation of its own. A
|
|
81
106
|
// standing written into its cells would be the owner's reach dressed as
|
|
@@ -97,7 +122,7 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
97
122
|
// standing, since the two share one namespace. On the ward itself the
|
|
98
123
|
// id is an owner, and only the root may unseat one, for the reason only
|
|
99
124
|
// the root may invite one: ownership moves by the root alone.
|
|
100
|
-
const being =
|
|
125
|
+
const being = named(args.being), id = named(args.id);
|
|
101
126
|
if (being === null || id === null)
|
|
102
127
|
return { error: 'no such being' };
|
|
103
128
|
if (being === w.pk && asker.id !== OWNER)
|
|
@@ -121,7 +146,7 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
121
146
|
// refused, for the reason knock is: it is a being to nobody outside, and
|
|
122
147
|
// unbooting it would be a ward deleting itself from inside its own map,
|
|
123
148
|
// leaving its owners bound to a door that is gone.
|
|
124
|
-
const being =
|
|
149
|
+
const being = named(args.being);
|
|
125
150
|
if (being === null || being === w.pk)
|
|
126
151
|
return { error: 'no such being' };
|
|
127
152
|
const removed = w.unboot(being);
|
|
@@ -130,22 +155,23 @@ export async function ownerAnswer(w, asker, method, args) {
|
|
|
130
155
|
return { error: 'unknown ask' };
|
|
131
156
|
}
|
|
132
157
|
// The ward's describe for its owner: its beings, their classes, a digest each,
|
|
133
|
-
// as she would describe herself to the owner.
|
|
158
|
+
// as she would describe herself to the owner. A being absent this run is
|
|
159
|
+
// listed too, with a null digest and absent true, so the owner can see her
|
|
160
|
+
// and take her out.
|
|
134
161
|
async function describe(w) {
|
|
135
162
|
const beings = {};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 };
|
|
163
|
+
// Every being is asked at once and each is bounded on her own. One being
|
|
164
|
+
// whose describe never settles is one null digest, not a ward that never
|
|
165
|
+
// answers its owner: the harbor learns the pk by this very ask at boot, so
|
|
166
|
+
// an unbounded wait here is a ward no restart can bring back.
|
|
167
|
+
const asked = [...w.doors].filter(([key]) => key !== w.pk);
|
|
168
|
+
const digests = await Promise.all(asked.map(async ([, door]) => (await within(DEFAULT.time, seen(door, { id: OWNER })))));
|
|
169
|
+
for (const [i, [key, door]] of asked.entries()) {
|
|
170
|
+
const d = digests[i];
|
|
171
|
+
put(beings, key, { class: door.cells.class ?? null, public: w.publicKey() === key, digest: d === LATE ? null : d });
|
|
148
172
|
}
|
|
173
|
+
for (const [key, cls] of Object.entries(w.absent()))
|
|
174
|
+
put(beings, key, { class: cls, public: w.publicKey() === key, digest: null, absent: true });
|
|
149
175
|
return {
|
|
150
176
|
asks: OWNER_ASKS,
|
|
151
177
|
notes: { pk: w.pk, beings },
|
package/dist/ward/partition.d.ts
CHANGED
|
@@ -39,6 +39,9 @@ export type Gone = {
|
|
|
39
39
|
announced: string | null;
|
|
40
40
|
};
|
|
41
41
|
export declare const GONE = 256;
|
|
42
|
+
export declare const MINTED = 8;
|
|
43
|
+
export declare const KNOCKS = 64;
|
|
44
|
+
export declare const prune: (knocks: Record<string, unknown>, answered: Record<string, true>) => void;
|
|
42
45
|
export type Partition = {
|
|
43
46
|
version: string;
|
|
44
47
|
beings: Record<string, Cells & {
|
package/dist/ward/partition.js
CHANGED
|
@@ -26,11 +26,116 @@ function upgrade(p) {
|
|
|
26
26
|
return p;
|
|
27
27
|
}
|
|
28
28
|
export const GONE = 256;
|
|
29
|
+
// The last few pks a relation minted, and no more. A relation rotates on
|
|
30
|
+
// every ask, so a list of all of them is a partition that grows for as long
|
|
31
|
+
// as she keeps talking and can never be written down. It lives here, beside
|
|
32
|
+
// the other bound on the durable shape, because it is a fact about what a
|
|
33
|
+
// partition may hold and not about how a ward mints.
|
|
34
|
+
export const MINTED = 8;
|
|
35
|
+
// A knock record is a key she minted for one invitation, kept until she takes
|
|
36
|
+
// it. Only take deletes one, so a being who knocks and never takes would grow
|
|
37
|
+
// her bind table for the life of the ward, one secret seed at a time. The
|
|
38
|
+
// list is bounded like `gone`: past the count the oldest go, and a relation
|
|
39
|
+
// that answered outlives one that never did, since an answered knock is one
|
|
40
|
+
// she may still take.
|
|
41
|
+
export const KNOCKS = 64;
|
|
42
|
+
export const prune = (knocks, answered) => {
|
|
43
|
+
const names = Object.keys(knocks);
|
|
44
|
+
if (names.length <= KNOCKS)
|
|
45
|
+
return;
|
|
46
|
+
const order = [...names.filter((n) => !answered[n]), ...names.filter((n) => answered[n])];
|
|
47
|
+
for (const old of order.slice(0, names.length - KNOCKS)) {
|
|
48
|
+
delete knocks[old];
|
|
49
|
+
delete answered[old];
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// The shape, read before anything acts on it. A partition this ward wrote is
|
|
53
|
+
// this shape by construction; one it adopted was written by a hand, another
|
|
54
|
+
// kit, or a file that was cut short, and every field below is one the ward,
|
|
55
|
+
// the door or the heirs then act on without looking again. A `spent` that is
|
|
56
|
+
// not an array rejects the door's promise on the first bound ask, a `mark`
|
|
57
|
+
// that is not a number honours every number ever sent and grows without
|
|
58
|
+
// bound, a being row that is null refuses the whole ward at birth by
|
|
59
|
+
// accident and says nothing about why. So the shape is read once, here, and
|
|
60
|
+
// what is not it throws with the path that failed, which names the trouble
|
|
61
|
+
// to whoever is holding the file.
|
|
62
|
+
//
|
|
63
|
+
// The rule, so the reader does not grow into a schema: read every field the
|
|
64
|
+
// ward acts on, and refuse only what would make it act wrongly. So `minted`
|
|
65
|
+
// must be a list, because she pushes to it, and the truth in `answered` is
|
|
66
|
+
// not read, because anything there says answered and nothing else follows.
|
|
67
|
+
// `last` is reserved and nobody reads it, so nobody reads its shape either;
|
|
68
|
+
// the run that gives it a reader gives it a line here. A being's own cells
|
|
69
|
+
// are hers, of any shape JSON carries, and only the two records the ward
|
|
70
|
+
// keeps inside them are read.
|
|
71
|
+
const fault = (path, want) => {
|
|
72
|
+
throw new Error(`partition ${path} is not ${want}`);
|
|
73
|
+
};
|
|
74
|
+
const table = (v, path) => (typeof v === 'object' && v !== null && !Array.isArray(v) ? v : fault(path, 'an object'));
|
|
75
|
+
const list = (v, path) => (Array.isArray(v) ? v : fault(path, 'an array'));
|
|
76
|
+
const text = (v, path) => void (typeof v === 'string' || fault(path, 'a string'));
|
|
77
|
+
const orNull = (v, path) => void (v === null || typeof v === 'string' || fault(path, 'a string or null'));
|
|
78
|
+
const whole = (v, path) => void (Number.isSafeInteger(v) || fault(path, 'a whole number'));
|
|
79
|
+
const flag = (v, path) => void (typeof v === 'boolean' || fault(path, 'true or false'));
|
|
80
|
+
function read(p) {
|
|
81
|
+
for (const [key, cells] of Object.entries(table(p.beings, 'beings'))) {
|
|
82
|
+
const c = table(cells, `beings.${key}`);
|
|
83
|
+
table(c.standings, `beings.${key}.standings`);
|
|
84
|
+
table(c.occupants, `beings.${key}.occupants`);
|
|
85
|
+
if (c.class !== undefined)
|
|
86
|
+
text(c.class, `beings.${key}.class`);
|
|
87
|
+
}
|
|
88
|
+
for (const [key, bind] of Object.entries(table(p.bind, 'bind'))) {
|
|
89
|
+
const b = table(bind, `bind.${key}`);
|
|
90
|
+
for (const [id, keys] of Object.entries(table(b.standings, `bind.${key}.standings`))) {
|
|
91
|
+
const w = `bind.${key}.standings.${id}`;
|
|
92
|
+
const s = table(keys, w);
|
|
93
|
+
text(s.ward, `${w}.ward`);
|
|
94
|
+
orNull(s.heir, `${w}.heir`);
|
|
95
|
+
text(s.current, `${w}.current`);
|
|
96
|
+
orNull(s.next, `${w}.next`);
|
|
97
|
+
whole(s.seq, `${w}.seq`);
|
|
98
|
+
}
|
|
99
|
+
for (const [id, pk] of Object.entries(table(b.occupants, `bind.${key}.occupants`)))
|
|
100
|
+
text(pk, `bind.${key}.occupants.${id}`);
|
|
101
|
+
for (const [name, keys] of Object.entries(table(b.knocks, `bind.${key}.knocks`))) {
|
|
102
|
+
const w = `bind.${key}.knocks.${name}`;
|
|
103
|
+
const k = table(keys, w);
|
|
104
|
+
text(k.current, `${w}.current`);
|
|
105
|
+
orNull(k.next, `${w}.next`);
|
|
106
|
+
flag(k.spoke, `${w}.spoke`);
|
|
107
|
+
flag(k.sent, `${w}.sent`);
|
|
108
|
+
whole(k.seq, `${w}.seq`);
|
|
109
|
+
}
|
|
110
|
+
table(b.answered, `bind.${key}.answered`);
|
|
111
|
+
for (const [i, pk] of list(b.minted, `bind.${key}.minted`).entries())
|
|
112
|
+
text(pk, `bind.${key}.minted[${i}]`);
|
|
113
|
+
}
|
|
114
|
+
for (const [pk, heir] of Object.entries(table(p.heirs, 'heirs'))) {
|
|
115
|
+
const h = table(heir, `heirs.${pk}`);
|
|
116
|
+
text(h.being, `heirs.${pk}.being`);
|
|
117
|
+
text(h.id, `heirs.${pk}.id`);
|
|
118
|
+
text(h.current, `heirs.${pk}.current`);
|
|
119
|
+
orNull(h.announced, `heirs.${pk}.announced`);
|
|
120
|
+
flag(h.fresh, `heirs.${pk}.fresh`);
|
|
121
|
+
whole(h.mark, `heirs.${pk}.mark`);
|
|
122
|
+
for (const [i, n] of list(h.spent, `heirs.${pk}.spent`).entries())
|
|
123
|
+
whole(n, `heirs.${pk}.spent[${i}]`);
|
|
124
|
+
}
|
|
125
|
+
for (const [pk, gone] of Object.entries(table(p.gone, 'gone'))) {
|
|
126
|
+
const g = table(gone, `gone.${pk}`);
|
|
127
|
+
text(g.current, `gone.${pk}.current`);
|
|
128
|
+
orNull(g.announced, `gone.${pk}.announced`);
|
|
129
|
+
}
|
|
130
|
+
orNull(p.public, 'public');
|
|
131
|
+
return p;
|
|
132
|
+
}
|
|
29
133
|
// The one place a ward reads state that another ward wrote. A fresh
|
|
30
134
|
// memory becomes a partition of this version; anything already written must
|
|
31
|
-
// say it is this version
|
|
32
|
-
// where a ward is allowed to be loud:
|
|
33
|
-
// that were made, and no ask has been
|
|
135
|
+
// say it is this version and be the shape this version names, and a ward that
|
|
136
|
+
// cannot read it does not boot. Birth is where a ward is allowed to be loud:
|
|
137
|
+
// silence is the door's word, for asks that were made, and no ask has been
|
|
138
|
+
// made yet.
|
|
34
139
|
export function open(memory) {
|
|
35
140
|
const p = memory;
|
|
36
141
|
if (Object.keys(memory).length > 0 && p.version !== VERSION) {
|
|
@@ -44,7 +149,7 @@ export function open(memory) {
|
|
|
44
149
|
p.heirs ??= {};
|
|
45
150
|
p.gone ??= {};
|
|
46
151
|
p.public ??= null;
|
|
47
|
-
return p;
|
|
152
|
+
return read(p);
|
|
48
153
|
}
|
|
49
154
|
// One record, reached by a key a being or an owner chose. A bare lookup finds
|
|
50
155
|
// every name on Object's prototype, so `__proto__` answers with the prototype
|
package/dist/ward/seal.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare function wardKey(seed: string | Uint8Array): Promise<WardKey>;
|
|
|
10
10
|
export declare const wardSignPk: (pk: string) => Uint8Array;
|
|
11
11
|
export declare const wardPadlock: (pk: string) => Uint8Array;
|
|
12
12
|
export declare const isWardPk: (pk: unknown) => pk is string;
|
|
13
|
+
export declare const isHex: (v: unknown) => v is string;
|
|
13
14
|
export declare function beingKey(seed: Uint8Array): Promise<{
|
|
14
15
|
seed: string;
|
|
15
16
|
pk: string;
|
package/dist/ward/seal.js
CHANGED
|
@@ -1,18 +1,45 @@
|
|
|
1
1
|
import { isDoorWord } from '../being/silence.js';
|
|
2
|
-
import { KEY, SIGNATURE, box, concat, hex, sha256, sign, signingPair, sealingPair, unbox, unhex, verify } from './arithmetic.js';
|
|
2
|
+
import { KEY, SIGNATURE, box, concat, derive, 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();
|
|
6
|
+
// The two labels a ward's keys are derived under. They are here and not in
|
|
7
|
+
// arithmetic.ts because a label is a decision and that file is arithmetic.
|
|
8
|
+
// `quo-seal` is taken: it is the label the message cipher derives its key and
|
|
9
|
+
// nonce under, from an agreement rather than from a seed, and two derivations
|
|
10
|
+
// answering to one name is how a kit is read wrong.
|
|
11
|
+
const WARD_SIGN = new TextEncoder().encode('quo-ward-sign');
|
|
12
|
+
const WARD_SEAL = new TextEncoder().encode('quo-ward-seal');
|
|
5
13
|
// The ward's key from its seed. Its pk on the wire is the signing pk then the padlock, 128 hex.
|
|
14
|
+
//
|
|
15
|
+
// One seed, two curves, and each secret derived from it under its own label.
|
|
16
|
+
// Fed the seed straight, the two scalars differ only because Ed25519 hashes
|
|
17
|
+
// what it is given and X25519 clamps it raw, which is an accident of the two
|
|
18
|
+
// designs and no separation at all: one secret would be doing two jobs with
|
|
19
|
+
// nothing said about it, and a second kit would have to reproduce a
|
|
20
|
+
// construction nobody named. HKDF-SHA-256 under a label is the separation
|
|
21
|
+
// said out loud, and it is what `vectors/framing.json` pins.
|
|
22
|
+
//
|
|
23
|
+
// Bytes are key material and text is not. A seed handed in as bytes of the
|
|
24
|
+
// key length is taken as it stands, which is what a harbor mints; anything
|
|
25
|
+
// else, of any length, is hashed to that length first. So a thirty-two
|
|
26
|
+
// character name is a name like any other, and not a key because it happened
|
|
27
|
+
// to be the right size.
|
|
6
28
|
export async function wardKey(seed) {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const p = await sealingPair(seed32);
|
|
29
|
+
const seed32 = typeof seed === 'string' ? await sha256(utf8.encode(seed)) : seed.length === KEY ? seed : await sha256(seed);
|
|
30
|
+
const s = await signingPair(await derive(seed32, WARD_SIGN, KEY));
|
|
31
|
+
const p = await sealingPair(await derive(seed32, WARD_SEAL, KEY));
|
|
11
32
|
return { sign: s.secret, padlock: p.secret, signPk: s.pk, padlockPk: p.pk, pk: hex(s.pk) + hex(p.pk) };
|
|
12
33
|
}
|
|
13
34
|
export const wardSignPk = (pk) => unhex(pk.slice(0, KEY * 2));
|
|
14
35
|
export const wardPadlock = (pk) => unhex(pk.slice(KEY * 2));
|
|
15
36
|
export const isWardPk = (pk) => typeof pk === 'string' && /^[0-9a-f]{128}$/.test(pk);
|
|
37
|
+
// Thirty-two bytes as hex, which is how this kit writes every single key: a
|
|
38
|
+
// heir pk, a heir secret, a being's own key, and a digest, which is the same
|
|
39
|
+
// thirty-two bytes of SHA-256. A ward pk is two of these and has its own
|
|
40
|
+
// shape above. The two are spelled here and nowhere else, so a door, a
|
|
41
|
+
// stance, a harbor and a conformance suite all refuse the same string.
|
|
42
|
+
export const isHex = (v) => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v);
|
|
16
43
|
// A being's relation key: a seed, and the pk it signs as.
|
|
17
44
|
export async function beingKey(seed) {
|
|
18
45
|
return { seed: hex(seed), pk: hex((await signingPair(seed)).pk) };
|
|
@@ -39,22 +66,23 @@ export async function openAsk(bytes, padlockSecret) {
|
|
|
39
66
|
// The heir, or null for the public being. Absent is not null: a payload
|
|
40
67
|
// that never named one is malformed, like any other missing field.
|
|
41
68
|
const to = payload.to;
|
|
42
|
-
if (to !== null && !(
|
|
69
|
+
if (to !== null && !isHex(to))
|
|
43
70
|
return null;
|
|
44
|
-
if (
|
|
71
|
+
if (!isHex(payload.by))
|
|
45
72
|
return null;
|
|
46
|
-
if (payload.next !== null && !(
|
|
73
|
+
if (payload.next !== null && !isHex(payload.next))
|
|
47
74
|
return null;
|
|
48
75
|
if (payload.method !== undefined && typeof payload.method !== 'string')
|
|
49
76
|
return null; // a name, or the empty ask. never a number, never an object.
|
|
50
77
|
if (payload.args !== undefined && (payload.args === null || typeof payload.args !== 'object' || Array.isArray(payload.args)))
|
|
51
78
|
return null; // args are one object, or absent. a string or a list is not an ask.
|
|
79
|
+
if (payload.args !== undefined && cellFault(payload.args, 'args') !== null)
|
|
80
|
+
return null; // and values all the way down: no key named __proto__, no nesting past the bound
|
|
52
81
|
if (!Number.isSafeInteger(payload.seq) || payload.seq < 1)
|
|
53
82
|
return null; // her count for this relation. one and up, and a whole number.
|
|
54
|
-
// The allowance
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
return null;
|
|
83
|
+
// The allowance is not read here. It is the door's, D2, and the door is
|
|
84
|
+
// its one reader: `spent` in allowance.ts says what a budget is and what
|
|
85
|
+
// is left of it, in one sentence, for a payload that opened like any other.
|
|
58
86
|
// The hop count, if this ask carries one: a whole number, never below
|
|
59
87
|
// zero. Absent is the ordinary ask and always will be.
|
|
60
88
|
if (payload.hops !== undefined && (!Number.isSafeInteger(payload.hops) || payload.hops < 0))
|
|
@@ -99,7 +127,7 @@ export async function openReply(bytes, ephemeralSecret, signPk) {
|
|
|
99
127
|
return isDoorWord(r.quo) ? { quo: r.quo } : null;
|
|
100
128
|
if (!Object.hasOwn(r, 'object') || r.object === undefined)
|
|
101
129
|
return null;
|
|
102
|
-
if (r.seen !== null && !(
|
|
130
|
+
if (r.seen !== null && !isHex(r.seen))
|
|
103
131
|
return null;
|
|
104
132
|
return { object: r.object, seen: r.seen };
|
|
105
133
|
}
|
package/dist/ward/stance.d.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
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 = {
|
|
5
5
|
pk: string;
|
|
6
|
+
live(): boolean;
|
|
6
7
|
mintKey(bind: Bind): Promise<{
|
|
7
8
|
seed: string;
|
|
8
9
|
pk: string;
|
|
9
10
|
}>;
|
|
10
11
|
openHeir(heir: string, being: string, id: string): void;
|
|
11
12
|
closeHeir(heir: string): void;
|
|
12
|
-
send(bind: Bind, keys: StandingKeys,
|
|
13
|
-
instantiate(
|
|
13
|
+
send(bind: Bind, keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
|
|
14
|
+
instantiate(className: string, key: string): string | null;
|
|
15
|
+
relate(key: string, id: string): Promise<Invitation | null>;
|
|
16
|
+
unmake(key: string): void;
|
|
17
|
+
wrote(): void;
|
|
14
18
|
};
|
|
15
19
|
export type SealedInvitation = {
|
|
16
20
|
ward: string;
|