@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/src/ward/stance.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// The one stance builder. Used for every being the ward boots. Nothing
|
|
3
3
|
// outer is in what she holds: ids in, values out, keys in the bind table.
|
|
4
|
-
import { silence, isSilence,
|
|
4
|
+
import { silence, isSilence, isWord, word, wordOf } from '../being/silence.ts';
|
|
5
5
|
import { digest } from '../being/digest.ts';
|
|
6
6
|
import { RESERVED_IDS } from '../being/types.ts';
|
|
7
7
|
import type { Answer, Cells, Invitation, JsonObject, Silence, Stance, Standing, Standings, Wanted, Word } from '../being/types.ts';
|
|
8
8
|
import { at, put, drop, type Bind, type StandingKeys } from './partition.ts';
|
|
9
9
|
import { allow, within, LATE } from './allowance.ts';
|
|
10
10
|
import { isWardPk, type ReplyPayload } from './seal.ts';
|
|
11
|
+
import { cellFault } from './cells.ts';
|
|
11
12
|
|
|
12
13
|
// What the stance needs from its ward, and no more.
|
|
13
14
|
export type Inside = {
|
|
@@ -20,6 +21,12 @@ export type Inside = {
|
|
|
20
21
|
send(bind: Bind, keys: StandingKeys, announce: boolean, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<ReplyPayload | Silence | Word>;
|
|
21
22
|
// a new being of this ward, as the owner's boot makes one. the key, or null.
|
|
22
23
|
instantiate(key: string, className: string): string | null;
|
|
24
|
+
// mint an occupant on a being of this ward, for the being who made her.
|
|
25
|
+
relate(key: string, id: string): Promise<Invitation | null>;
|
|
26
|
+
// take a being of this ward out again, with every relation she holds.
|
|
27
|
+
unmake(key: string): void;
|
|
28
|
+
// the bind table was written: a record put, dropped or moved on.
|
|
29
|
+
wrote(): void;
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
// An invitation as it travels: the far ward's pk, the heir pk, the heir's secret seed. Or just the ward, for its public being.
|
|
@@ -72,9 +79,9 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
72
79
|
// Every ask a being makes, bounded. The bound is outside the lane on
|
|
73
80
|
// purpose: an ask that waits its turn behind a relation that comes back
|
|
74
81
|
// round never reaches a send, and a bound that only watched the wire would
|
|
75
|
-
// never see it hang. A bound that ran out is
|
|
76
|
-
// unreached promises nothing was delivered and is safe to ask
|
|
77
|
-
// wait that gave up knows no such thing.
|
|
82
|
+
// never see it hang. A bound that ran out is the word late, never
|
|
83
|
+
// unreached: unreached promises nothing was delivered and is safe to ask
|
|
84
|
+
// again, and a wait that gave up knows no such thing.
|
|
78
85
|
//
|
|
79
86
|
// The bell rings before the work is reached, too: an ask that was still
|
|
80
87
|
// waiting its turn when the wait ran out is never sent. Sent then, it would
|
|
@@ -89,53 +96,89 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
89
96
|
return out === LATE ? word('late') : out;
|
|
90
97
|
};
|
|
91
98
|
|
|
99
|
+
// What one send came back as, read in one place for every path that sends.
|
|
100
|
+
// A reply that opened and was signed by the far ward is the door speaking,
|
|
101
|
+
// whatever it said: an object, its silence, or a word. Bytes that were not
|
|
102
|
+
// Quo's, late and unreached are not the door speaking, and say nothing
|
|
103
|
+
// about whether it heard.
|
|
104
|
+
type Read = { answer: Answer; door: boolean; seen: string | null };
|
|
105
|
+
const read = (r: ReplyPayload | Silence | Word): Read => {
|
|
106
|
+
if (isSilence(r) || isWord(r)) return { answer: r, door: false, seen: null };
|
|
107
|
+
if ('silence' in r) return { answer: silence, door: true, seen: null };
|
|
108
|
+
if ('quo' in r) return { answer: word(r.quo), door: true, seen: null }; // the far door's word for her, sealed to her lid
|
|
109
|
+
return { answer: r.object, door: true, seen: r.seen };
|
|
110
|
+
};
|
|
111
|
+
// Whether the door spent the heir this send spoke under. An object, the
|
|
112
|
+
// door's silence and threw all mean it was honoured. A refusal word wrote
|
|
113
|
+
// nothing and says so. Anything that is not the door speaking says nothing.
|
|
114
|
+
const honoured = (r: Read): boolean => r.door && !(isWord(r.answer) && wordOf(r.answer) !== 'threw');
|
|
115
|
+
const answered = (r: Read): boolean => r.door && !isSilence(r.answer) && !isWord(r.answer);
|
|
116
|
+
|
|
92
117
|
const send = async (keys: StandingKeys, method: string | undefined, args: JsonObject, wanted: Wanted | undefined, rec?: Cells['standings'][string]): Promise<Answer> => {
|
|
93
|
-
const r = await inside.send(bind, keys, true, method, args, wanted);
|
|
94
|
-
if (isSilence(r) || isWord(r)) return r;
|
|
95
|
-
if ('silence' in r) return silence;
|
|
96
|
-
if ('quo' in r) return word(r.quo); // the far door's word for her, sealed to her lid
|
|
118
|
+
const r = read(await inside.send(bind, keys, true, method, args, wanted));
|
|
97
119
|
if (rec && method !== undefined && r.seen !== null) rec.seen = r.seen; // the digest rode along
|
|
98
|
-
return r.
|
|
120
|
+
return r.answer;
|
|
99
121
|
};
|
|
100
122
|
|
|
101
123
|
const knocking = async (inv: SealedInvitation, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
|
|
102
124
|
const name = nameOf(inv);
|
|
125
|
+
// Taken while this knock waited its turn: the relation lives in the
|
|
126
|
+
// standing now and speaks on its lane, under its keys and its count.
|
|
127
|
+
const taken = takenAs(name);
|
|
128
|
+
if (taken !== undefined) {
|
|
129
|
+
const keys = at(bind.standings, taken);
|
|
130
|
+
if (!keys) return word('dropped');
|
|
131
|
+
return lane(`ask:${taken}`, () => asking(taken, keys, at(cells.standings, taken), method, args, wanted));
|
|
132
|
+
}
|
|
103
133
|
// her own key for this relation, minted once: rotation two. reused if she knocks again.
|
|
104
134
|
const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// spent
|
|
109
|
-
//
|
|
135
|
+
// The first knock on a heir speaks as the heir, the key the inviter
|
|
136
|
+
// handed out, and announces her own. Once the door has honoured it the
|
|
137
|
+
// heir is spent, whatever she then said, and her own key speaks; a heir
|
|
138
|
+
// spent once stays spent.
|
|
139
|
+
//
|
|
140
|
+
// She sent once as the heir and the door did not speak: nothing came
|
|
141
|
+
// back, or bytes that were not Quo's, or the wait ran out. Either the
|
|
142
|
+
// door heard, spent the heir and rotated to the key she announced, or
|
|
143
|
+
// nothing arrived and the heir still stands. Only the far door knows,
|
|
144
|
+
// and it will not say.
|
|
110
145
|
//
|
|
111
146
|
// So she asks, and the asking tells her. Under her own key: if the door
|
|
112
|
-
// heard, that key is the one it admits, and she is answered
|
|
113
|
-
// not, the key means nothing there, the ask is
|
|
114
|
-
// that door writes nothing
|
|
115
|
-
// she knocks as the heir, as she would
|
|
116
|
-
// one case where a reply was lost, and
|
|
147
|
+
// heard, that key is the one it admits, and she is answered, or told the
|
|
148
|
+
// door's word. If it did not, the key means nothing there, the ask is
|
|
149
|
+
// refused, and a refusal at that door writes nothing: the heir is
|
|
150
|
+
// untouched and still speaks. Then she knocks as the heir, as she would
|
|
151
|
+
// have. One extra round trip in the one case where a reply was lost, and
|
|
152
|
+
// the relation is not stranded.
|
|
117
153
|
//
|
|
118
154
|
// No stranger gains anything by it: whoever holds the invitation could
|
|
119
155
|
// always knock as the heir, and her own key is admitted only where the
|
|
120
156
|
// door already bound it to her.
|
|
121
157
|
if (inv.heir !== undefined && !k.spoke && k.sent) {
|
|
122
158
|
const own: StandingKeys = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
|
|
123
|
-
const r = await inside.send(bind, own, true, method, args, wanted);
|
|
159
|
+
const r = read(await inside.send(bind, own, true, method, args, wanted));
|
|
124
160
|
k.seq = own.seq;
|
|
125
161
|
k.current = own.current;
|
|
126
162
|
k.next = own.next;
|
|
127
|
-
|
|
128
|
-
if (!
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return r.
|
|
163
|
+
inside.wrote();
|
|
164
|
+
if (!r.door) {
|
|
165
|
+
// Nothing the door said. Unreached and late are hers to hear now; bytes
|
|
166
|
+
// that were not Quo's leave her where she was, and the heir is tried.
|
|
167
|
+
if (!isSilence(r.answer)) return r.answer;
|
|
168
|
+
} else if (!isSilence(r.answer)) {
|
|
169
|
+
// The door spoke to her own key, so it heard the first knock: this is
|
|
170
|
+
// the answer, or the door's word for her. Its silence alone cannot be
|
|
171
|
+
// told from a refusal, so silence is followed by the knock as the heir,
|
|
172
|
+
// which the door then refuses if it had heard, and she hears silence.
|
|
173
|
+
k.spoke = true;
|
|
174
|
+
if (answered(r)) bind.answered[name] = true;
|
|
175
|
+
return r.answer;
|
|
132
176
|
}
|
|
133
|
-
// Refused. The door never heard her, so the heir is still hers to spend.
|
|
134
177
|
}
|
|
135
178
|
|
|
136
179
|
const first = inv.heir !== undefined && !k.spoke;
|
|
137
180
|
const keys: StandingKeys = first ? { ward: inv.ward, heir: inv.heir!, current: inv.secret!, next: k.current, seq: k.seq } : { ward: inv.ward, heir: inv.heir ?? null, current: k.current, next: k.next, seq: k.seq };
|
|
138
|
-
const r = await inside.send(bind, keys, true, method, args, wanted);
|
|
181
|
+
const r = read(await inside.send(bind, keys, true, method, args, wanted));
|
|
139
182
|
k.sent = true; // bytes went out. whether they arrived is the far door's to know.
|
|
140
183
|
// The number she spoke under, and the key she announced, kept whoever
|
|
141
184
|
// answered and kept when nobody did. A reply lost on the way back is a
|
|
@@ -146,16 +189,10 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
146
189
|
k.current = keys.current;
|
|
147
190
|
k.next = keys.next;
|
|
148
191
|
}
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
k.spoke = !(!isSilence(r) && !isWord(r) && 'quo' in r && r.quo !== 'threw');
|
|
154
|
-
if (isSilence(r) || isWord(r)) return r;
|
|
155
|
-
if ('silence' in r) return silence;
|
|
156
|
-
if ('quo' in r) return word(r.quo);
|
|
157
|
-
bind.answered[name] = true;
|
|
158
|
-
return r.object;
|
|
192
|
+
if (honoured(r)) k.spoke = true;
|
|
193
|
+
if (answered(r)) bind.answered[name] = true;
|
|
194
|
+
inside.wrote();
|
|
195
|
+
return r.answer;
|
|
159
196
|
};
|
|
160
197
|
|
|
161
198
|
// The id she took this relation under, if she took it. One relation is one
|
|
@@ -172,7 +209,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
172
209
|
// standing's keys and its count. A second lane here would speak for the
|
|
173
210
|
// same relation, and the two would refuse each other.
|
|
174
211
|
const id = takenAs(nameOf(inv));
|
|
175
|
-
if (id !== undefined) return
|
|
212
|
+
if (id !== undefined) return standingAt(id).ask(method, args, wanted);
|
|
176
213
|
return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
|
|
177
214
|
},
|
|
178
215
|
// Take consumes. Until now the relation lived in the knock record, under
|
|
@@ -206,12 +243,14 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
206
243
|
put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
|
|
207
244
|
delete bind.knocks[name];
|
|
208
245
|
delete bind.answered[name];
|
|
246
|
+
inside.wrote();
|
|
209
247
|
return Promise.resolve(id);
|
|
210
248
|
});
|
|
211
249
|
},
|
|
212
250
|
remove: (id: string): void => {
|
|
213
251
|
drop(cells.standings, id);
|
|
214
252
|
drop(bind.standings, id);
|
|
253
|
+
inside.wrote();
|
|
215
254
|
},
|
|
216
255
|
};
|
|
217
256
|
|
|
@@ -228,13 +267,18 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
228
267
|
},
|
|
229
268
|
});
|
|
230
269
|
|
|
270
|
+
// The record an ask writes into is the one it was issued on. A standing
|
|
271
|
+
// dropped and re-taken under the same id while the ask was in flight is
|
|
272
|
+
// another relation, and the blueprint that came back is not hers.
|
|
231
273
|
const asking = async (id: string, keys: StandingKeys, rec: Cells['standings'][string] | undefined, method: string | undefined, args: JsonObject, wanted: Wanted | undefined): Promise<Answer> => {
|
|
232
274
|
const out = await send(keys, method, args, wanted, rec);
|
|
233
275
|
if (method === undefined && !isSilence(out) && !isWord(out)) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
276
|
+
// A blueprint her cells cannot keep, nested past the bound or carrying
|
|
277
|
+
// a key no harbor writes, is not Quo's bytes: silence, and nothing written.
|
|
278
|
+
if (cellFault(out, 'blueprint') !== null) return silence;
|
|
279
|
+
if (rec && at(cells.standings, id) === rec) {
|
|
280
|
+
rec.blueprint = out as never;
|
|
281
|
+
rec.digest = rec.seen = await digest(out);
|
|
238
282
|
}
|
|
239
283
|
}
|
|
240
284
|
return out;
|
|
@@ -251,19 +295,43 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
251
295
|
cells,
|
|
252
296
|
// A being may make. The ward refuses what it refuses the owner: a key
|
|
253
297
|
// already booted, a class the harbor does not hold, and a throw at birth
|
|
254
|
-
// leaves the partition as it was. Reaching into
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
298
|
+
// leaves the partition as it was. Reaching into another is still the
|
|
299
|
+
// owner's alone: what a maker may have is a relation to what she made,
|
|
300
|
+
// by naming an id, and it is made the way every relation is. The being
|
|
301
|
+
// made mints an occupant for her maker under the maker's own key, so she
|
|
302
|
+
// knows who made her by that name and by nothing else; her maker knocks,
|
|
303
|
+
// takes it under the id she gave, and holds one standing like any other.
|
|
304
|
+
// Nothing half-lives: a relation that could not be made unmakes the
|
|
305
|
+
// being, who is a moment old and known to nobody.
|
|
306
|
+
boot: async (className: string, made: string, id?: string): Promise<string | null> => {
|
|
307
|
+
if (id !== undefined && (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))) return null;
|
|
308
|
+
const born = inside.instantiate(made, className);
|
|
309
|
+
if (born === null || id === undefined) return born;
|
|
310
|
+
const inv = await inside.relate(born, key);
|
|
311
|
+
if (inv) {
|
|
312
|
+
const out = await calls.knock(inv);
|
|
313
|
+
if (!isSilence(out) && !isWord(out) && (await calls.take(id, inv)) === id) return born;
|
|
314
|
+
}
|
|
315
|
+
inside.unmake(born);
|
|
316
|
+
return null;
|
|
317
|
+
},
|
|
258
318
|
occupants: {
|
|
259
319
|
// rotation one: the ward mints the occupant's first key and gives the secret away.
|
|
260
320
|
// it keeps the pk beside the id and nothing else. the invitation IS the key.
|
|
261
|
-
invite: async (id: string): Promise<Invitation | null> => {
|
|
321
|
+
invite: async (id: string, notes?: JsonObject): Promise<Invitation | null> => {
|
|
262
322
|
if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
|
|
263
|
-
|
|
323
|
+
// The key first, and nothing written until it exists: a record put
|
|
324
|
+
// before the mint would be a record a remove in the meantime drops
|
|
325
|
+
// with no heir to close, and the heir opened after it would name an
|
|
326
|
+
// id nobody holds, for a later invite under that id to resurrect.
|
|
264
327
|
const k = await inside.mintKey(bind);
|
|
328
|
+
if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null; // taken while the key was minted
|
|
329
|
+
// The notes are the terms the inviter minted under, hers to read on her
|
|
330
|
+
// gate. She may write more later; nobody outside ever writes them.
|
|
331
|
+
put(cells.occupants, id, { id, notes: notes ? { ...notes } : {} });
|
|
265
332
|
inside.openHeir(k.pk, key, id);
|
|
266
333
|
put(bind.occupants, id, k.pk);
|
|
334
|
+
inside.wrote();
|
|
267
335
|
return { ward: inside.pk, heir: k.pk, secret: k.seed };
|
|
268
336
|
},
|
|
269
337
|
remove: (id: string): void => {
|
|
@@ -271,6 +339,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
|
|
|
271
339
|
const heir = at(bind.occupants, id);
|
|
272
340
|
drop(bind.occupants, id);
|
|
273
341
|
if (heir) inside.closeHeir(heir); // the heir dies with the id
|
|
342
|
+
inside.wrote();
|
|
274
343
|
},
|
|
275
344
|
},
|
|
276
345
|
standings,
|
package/src/ward/ward.ts
CHANGED
|
@@ -16,9 +16,11 @@ import { ownerAnswer } from './owner.ts';
|
|
|
16
16
|
import { allow, within, LATE } from './allowance.ts';
|
|
17
17
|
import { beingKey, openReply, sealAsk, wardKey, wardPadlock, wardSignPk, type ReplyPayload, type WardKey } from './seal.ts';
|
|
18
18
|
import { unhex } from './arithmetic.ts';
|
|
19
|
-
import { guardCells, cellFault } from './cells.ts';
|
|
19
|
+
import { guardCells, unguarded, cellFault } from './cells.ts';
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// One being of the ward as the ward holds her: her door, her bind table, her
|
|
22
|
+
// stance and her cells. The owner's asks see this and nothing more.
|
|
23
|
+
export type Booted = Door & { bind: Bind; stance: Stance; cells: Cells & { class?: string } };
|
|
22
24
|
|
|
23
25
|
// How many minted pks she keeps behind her. Enough to name the keys in play.
|
|
24
26
|
const MINTED = 8;
|
|
@@ -47,23 +49,24 @@ class Self implements BeingLike {
|
|
|
47
49
|
this.g = ground;
|
|
48
50
|
this.key = key;
|
|
49
51
|
this.pk = key.pk;
|
|
50
|
-
this.p = open(ground.memory); // the partition
|
|
51
|
-
this.heirs = new Heirs(this.p);
|
|
52
|
+
this.p = open(ground.memory); // the partition: the ward's own state, and every being's row
|
|
53
|
+
this.heirs = new Heirs(this.p, () => this.#wrote());
|
|
52
54
|
this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
|
|
53
55
|
this.#boot(this.pk, this); // the ward's ward is itself
|
|
54
56
|
// a restart is silent: every being in the cells is constructed again, unasked.
|
|
55
|
-
// A throw on a boot the owner asked for is that boot's
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
57
|
+
// A throw on a boot the owner asked for is that boot's error, and nothing
|
|
58
|
+
// half-lives. This boot nobody asked for, and there is nobody to hand an
|
|
59
|
+
// error to. Any constructor may throw, a default that is not a value, an
|
|
60
|
+
// author's mistake, a shape she does not recognise after an upgrade, and
|
|
61
|
+
// let through here it is one being taking down the ward and every other
|
|
62
|
+
// being in it, on a restart. She is absent instead: no door, so silence
|
|
63
|
+
// at the door, and her cells sit untouched, waiting for the run that can
|
|
64
|
+
// read them. Her row stays hers: nobody boots another class over it, the
|
|
65
|
+
// owner sees her as absent, and the owner may unboot her.
|
|
63
66
|
for (const [k, c] of Object.entries(this.p.beings)) {
|
|
64
67
|
if (k === this.pk || !c.class) continue;
|
|
65
68
|
try {
|
|
66
|
-
this.#
|
|
69
|
+
this.#boot(k, c.class);
|
|
67
70
|
} catch {
|
|
68
71
|
/* she is not here this run. the ward is, and so is everyone else. */
|
|
69
72
|
}
|
|
@@ -89,10 +92,14 @@ class Self implements BeingLike {
|
|
|
89
92
|
{
|
|
90
93
|
pk: this.pk,
|
|
91
94
|
doors: this.doors,
|
|
95
|
+
absent: () => this.#absent(),
|
|
92
96
|
publicKey: () => this.p.public,
|
|
93
97
|
instantiate: (key, className) => this.#instantiate(key, className),
|
|
94
98
|
unboot: (key) => this.#unboot(key),
|
|
95
|
-
setPublic: (key) =>
|
|
99
|
+
setPublic: (key) => {
|
|
100
|
+
this.p.public = key;
|
|
101
|
+
this.#wrote();
|
|
102
|
+
},
|
|
96
103
|
},
|
|
97
104
|
asker,
|
|
98
105
|
method,
|
|
@@ -101,11 +108,37 @@ class Self implements BeingLike {
|
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
// ---- ward functions. on the object. no stance reaches them.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
|
|
112
|
+
// The partition was written. The harbor is told, and nothing more: what it
|
|
113
|
+
// does with the word is its own, and the ward never learns.
|
|
114
|
+
#wrote(): void {
|
|
115
|
+
this.g.wrote?.();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The rows no door holds this run: beings whose class threw at birth or
|
|
119
|
+
// is not the harbor's to give. Key to class.
|
|
120
|
+
#absent(): Record<string, string | null> {
|
|
121
|
+
const out: Record<string, string | null> = {};
|
|
122
|
+
for (const [k, c] of Object.entries(this.p.beings)) if (k !== this.pk && !this.doors.has(k)) put(out, k, c.class ?? null);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A new being under a key nobody holds. A key with a row in the partition
|
|
127
|
+
// is booted, whether a door holds it this run or not: an absent being's
|
|
128
|
+
// row is hers, with every relation in it, and no class is booted over it.
|
|
129
|
+
// A throw at birth is that boot's refusal and nothing half-lives; the
|
|
130
|
+
// ward says which, since the owner hears objects.
|
|
131
|
+
#instantiate(key: string, className: string): Booted | 'threw' | null {
|
|
132
|
+
if (typeof key !== 'string' || typeof className !== 'string' || !key || this.doors.has(key) || at(this.p.beings, key)) return null;
|
|
133
|
+
let door: Booted | null;
|
|
134
|
+
try {
|
|
135
|
+
door = this.#boot(key, className);
|
|
136
|
+
} catch {
|
|
137
|
+
return 'threw';
|
|
138
|
+
}
|
|
107
139
|
if (!door) return null;
|
|
108
|
-
door.cells.class = className; // so a restart finds her
|
|
140
|
+
unguarded(door.cells).class = className; // so a restart finds her. the ward's key, written behind her guard
|
|
141
|
+
this.#wrote();
|
|
109
142
|
return door;
|
|
110
143
|
}
|
|
111
144
|
|
|
@@ -115,20 +148,30 @@ class Self implements BeingLike {
|
|
|
115
148
|
// rather than meeting a being who is simply not there any more. What is
|
|
116
149
|
// left after that is her cells and her bind table, and they go with her:
|
|
117
150
|
// a row naming a being no door holds would be read on the next restart
|
|
118
|
-
// and boot her again.
|
|
151
|
+
// and boot her again. An absent being leaves the same way: she has no
|
|
152
|
+
// stance to speak for her, so the ward closes her heirs itself.
|
|
119
153
|
#unboot(key: string): string[] | null {
|
|
154
|
+
if (key === this.pk) return null;
|
|
120
155
|
const door = this.doors.get(key);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
156
|
+
const cells = door?.cells ?? at(this.p.beings, key);
|
|
157
|
+
if (!cells) return null;
|
|
158
|
+
const ids = (r: unknown) => (r !== null && typeof r === 'object' ? Object.keys(r) : []);
|
|
159
|
+
const occupants = ids(cells.occupants),
|
|
160
|
+
standings = ids(cells.standings);
|
|
161
|
+
if (door) {
|
|
162
|
+
for (const id of occupants) door.stance.occupants.remove(id);
|
|
163
|
+
for (const id of standings) door.stance.standings.remove(id);
|
|
164
|
+
} else {
|
|
165
|
+
const bind = at(this.p.bind, key);
|
|
166
|
+
for (const heir of Object.values(bind?.occupants ?? {})) this.heirs.close(heir);
|
|
167
|
+
}
|
|
126
168
|
// A ward whose public being is gone answers arrivals for no heir with
|
|
127
169
|
// silence, as a ward that never had one does.
|
|
128
170
|
if (this.p.public === key) this.p.public = null;
|
|
129
171
|
drop(this.p.beings, key);
|
|
130
172
|
drop(this.p.bind, key);
|
|
131
173
|
this.doors.delete(key);
|
|
174
|
+
this.#wrote();
|
|
132
175
|
return [...occupants, ...standings];
|
|
133
176
|
}
|
|
134
177
|
|
|
@@ -136,7 +179,7 @@ class Self implements BeingLike {
|
|
|
136
179
|
// harbor does not know makes no being, and a boot that made nobody must
|
|
137
180
|
// leave the partition as it found it.
|
|
138
181
|
#boot(key: string, classNameOrSelf: string | Self): Booted | null {
|
|
139
|
-
const cells = guardCells(at(this.p.beings, key) ?? emptyCells());
|
|
182
|
+
const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
|
|
140
183
|
const bind = at(this.p.bind, key) ?? emptyBind();
|
|
141
184
|
const stance = buildStance(
|
|
142
185
|
{
|
|
@@ -148,23 +191,27 @@ class Self implements BeingLike {
|
|
|
148
191
|
// A throw at birth is null to her, not a throw in her method: she asked
|
|
149
192
|
// for a being and got none, and her own answer is still hers to give.
|
|
150
193
|
instantiate: (k, className) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
194
|
+
const made = this.#instantiate(k, className);
|
|
195
|
+
return made && made !== 'threw' ? made.key : null;
|
|
156
196
|
},
|
|
197
|
+
// The two halves of a maker's way back, each the ward's own call: the
|
|
198
|
+
// being made mints an occupant for her maker, and a relation that
|
|
199
|
+
// never came about takes her out again.
|
|
200
|
+
relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
|
|
201
|
+
unmake: (k) => void this.#unboot(k),
|
|
202
|
+
wrote: () => this.#wrote(),
|
|
157
203
|
},
|
|
158
204
|
key,
|
|
159
205
|
cells,
|
|
160
206
|
bind,
|
|
161
207
|
);
|
|
162
208
|
const being = classNameOrSelf === this ? this : this.g.instantiate(classNameOrSelf as string, stance);
|
|
163
|
-
if (!being) return null;
|
|
209
|
+
if (!being || typeof being.answer !== 'function') return null; // a being is anything with an answer, and nothing without one
|
|
164
210
|
put(this.p.beings, key, cells);
|
|
165
211
|
put(this.p.bind, key, bind);
|
|
166
212
|
const door: Booted = { key, cells, bind, stance, being };
|
|
167
213
|
this.doors.set(key, door);
|
|
214
|
+
this.#wrote();
|
|
168
215
|
return door;
|
|
169
216
|
}
|
|
170
217
|
|
|
@@ -176,6 +223,7 @@ class Self implements BeingLike {
|
|
|
176
223
|
// but the keys of the moment.
|
|
177
224
|
bind.minted.push(k.pk);
|
|
178
225
|
if (bind.minted.length > MINTED) bind.minted.splice(0, bind.minted.length - MINTED);
|
|
226
|
+
this.#wrote();
|
|
179
227
|
return k;
|
|
180
228
|
}
|
|
181
229
|
|
|
@@ -202,10 +250,12 @@ class Self implements BeingLike {
|
|
|
202
250
|
// Args that will not go through the seal are args that never left. A
|
|
203
251
|
// being holds three answers and a throw is not one of them, so an ask
|
|
204
252
|
// that cannot be sealed is told what is true of it: no door was reached.
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
253
|
+
// One object of values, by the rule her cells keep and the far door
|
|
254
|
+
// reads: a Date would arrive as a string, a function would not arrive at
|
|
255
|
+
// all, a list or a string would be refused as a stranger's bytes, and
|
|
256
|
+
// she would never know the far being was asked something other than
|
|
257
|
+
// what she said.
|
|
258
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args) || cellFault(args, 'args') !== null) return unreached();
|
|
209
259
|
let sealed;
|
|
210
260
|
try {
|
|
211
261
|
sealed = await sealAsk(keys.heir, { by, next, seq, time: budget.time, method, args }, unhex(keys.current), wardPadlock(keys.ward), this.g.random(32));
|
|
@@ -237,6 +287,7 @@ class Self implements BeingLike {
|
|
|
237
287
|
keys.current = keys.next; // the far door holds `next` as announced. move to it.
|
|
238
288
|
keys.next = null;
|
|
239
289
|
}
|
|
290
|
+
this.#wrote(); // the count moved, and the keys may have
|
|
240
291
|
return reply;
|
|
241
292
|
}
|
|
242
293
|
}
|
package/vectors/arithmetic.json
CHANGED
|
@@ -65,6 +65,13 @@
|
|
|
65
65
|
"signature": "60aa62f7129c8fdeba710872fa10016dae1aa12df0f31969a6baabf60b2ebc3d0822cc9a8b1eb12246076b2eea69d381a03c2f581c05ba6bcca3b4ba80c2f504",
|
|
66
66
|
"refuses": true
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
"name": "a zero signature under a non-canonical small-order public key, y equal to p, is silence",
|
|
70
|
+
"voice": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
|
71
|
+
"message": "62792077686f736520617574686f72697479",
|
|
72
|
+
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
73
|
+
"refuses": true
|
|
74
|
+
},
|
|
68
75
|
{
|
|
69
76
|
"name": "an X25519 agreement",
|
|
70
77
|
"secret": "fcfabee71b7a33993cca5579e6a273ffd1c62cc1749cbf1c9049f599e44f6477",
|
package/vectors/framing.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"corpus": "@quo-systems/quo",
|
|
3
3
|
"encoding": "hex",
|
|
4
4
|
"area": "framing",
|
|
5
|
-
"note": "The formats a second kit must reproduce byte for byte: the ward pk, the digest, the signed ask body, the sealed ask, the sealed reply, the invitation and the knock. Every random input is fixed here, so every output is fixed. The ask body is JSON.stringify of the payload, which is not canonical: these vectors pin the key order sealAsk writes, to, by, next, seq, time, method, args; hops is in no vector, because nothing sets it. A kit that emits another order still interoperates, because a door verifies the bytes it received, but it will not reproduce these vectors. An invitation is JSON too, ward, heir, secret, and a heir secret is the seed of a being key. A knock is an ask whose by is the heir pk, signed with the heir secret, whose next is the knocker's own key, and whose to is the heir; its answer is an ordinary reply, and the heir is spent by it. A reply is one of three shapes: an object with its digest, silence, or the ward's word to a key the door has bound, { quo: word }, where word is one of removed, absent, unannounced, repeated, threw. The ward here is the one from the ward pk vector, and the heir secret is the first entropy it draws, so the invitation it mints and the knock its door binds are these bytes.",
|
|
5
|
+
"note": "The formats a second kit must reproduce byte for byte: the ward pk, the digest, the signed ask body, the sealed ask, the sealed reply, the invitation and the knock. Every random input is fixed here, so every output is fixed. The ask body is JSON.stringify of the payload with the heir it is for in front, which is not canonical: these vectors pin the key order sealAsk writes, to, by, next, seq, time, method, args, and to is signed with the rest, null for nobody; hops is in no vector, because nothing sets it. The two signed bodies are the bytes inside the two sealed asks. A kit that emits another order still interoperates, because a door verifies the bytes it received, but it will not reproduce these vectors. An invitation is JSON too, ward, heir, secret, and a heir secret is the seed of a being key. A knock is an ask whose by is the heir pk, signed with the heir secret, whose next is the knocker's own key, and whose to is the heir; its answer is an ordinary reply, and the heir is spent by it. A reply is one of three shapes: an object with its digest, silence, or the ward's word to a key the door has bound, { quo: word }, where word is one of removed, absent, unannounced, repeated, threw. The ward here is the one from the ward pk vector, and the heir secret is the first entropy it draws, so the invitation it mints and the knock its door binds are these bytes.",
|
|
6
6
|
"vectors": [
|
|
7
7
|
{
|
|
8
8
|
"name": "a ward pk from a fixed seed",
|
|
@@ -23,11 +23,20 @@
|
|
|
23
23
|
"digest": "269c9e90f39f6fcae9deab916e3a4148788dec5ba0d06023edc8e52180c773db"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
"name": "the signed ask body, and its signature",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
26
|
+
"name": "the signed ask body for nobody, and its signature: to is inside, first",
|
|
27
|
+
"to": "",
|
|
28
|
+
"payload": "{\"to\":null,\"by\":\"a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0\",\"next\":null,\"seq\":1,\"time\":30000,\"method\":\"hello\",\"args\":{\"name\":\"quo\"}}",
|
|
29
|
+
"body": "7b22746f223a6e756c6c2c226279223a2261303961613566343761363735393830326666393535663864633264326131346135633939643233626539376638363431323766663933383334353561346630222c226e657874223a6e756c6c2c22736571223a312c2274696d65223a33303030302c226d6574686f64223a2268656c6c6f222c2261726773223a7b226e616d65223a2271756f227d7d",
|
|
30
|
+
"secret": "2222222222222222222222222222222222222222222222222222222222222222",
|
|
31
|
+
"signature": "535c50354632d607fc5b68d081a0191ebf85c296b6c7e67b7b0dcc6030d569673dfe35f164b6b2eb3558c4454c32aabc5586bd82e305401a82fda7a166b91906"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "the signed ask body to a heir, and its signature: the same payload, to named",
|
|
35
|
+
"to": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
36
|
+
"payload": "{\"to\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"by\":\"a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0\",\"next\":null,\"seq\":1,\"time\":30000,\"method\":\"hello\",\"args\":{\"name\":\"quo\"}}",
|
|
37
|
+
"body": "7b22746f223a2261616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161222c226279223a2261303961613566343761363735393830326666393535663864633264326131346135633939643233626539376638363431323766663933383334353561346630222c226e657874223a6e756c6c2c22736571223a312c2274696d65223a33303030302c226d6574686f64223a2268656c6c6f222c2261726773223a7b226e616d65223a2271756f227d7d",
|
|
29
38
|
"secret": "2222222222222222222222222222222222222222222222222222222222222222",
|
|
30
|
-
"signature": "
|
|
39
|
+
"signature": "1f0e63f365dd09bff2c070091ea209d14eefec1026bf210dba87a4a18135fd90107c9879177695f0b4a899e67777b75d6001229e0875657cfc9a2cd197880500"
|
|
31
40
|
},
|
|
32
41
|
{
|
|
33
42
|
"name": "a sealed ask to a heir: the ephemeral pk, then the box, and nothing outside it",
|