@quo-systems/quo 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +9 -6
  2. package/SPEC.md +235 -137
  3. package/dist/being/being.d.ts +22 -0
  4. package/dist/being/being.js +78 -0
  5. package/dist/being/digest.d.ts +3 -0
  6. package/dist/being/digest.js +19 -0
  7. package/dist/being/index.d.ts +5 -0
  8. package/dist/being/index.js +6 -0
  9. package/dist/being/silence.d.ts +10 -0
  10. package/dist/being/silence.js +29 -0
  11. package/dist/being/types.d.ts +84 -0
  12. package/dist/being/types.js +16 -0
  13. package/dist/conformance/assert.d.ts +11 -0
  14. package/dist/conformance/assert.js +72 -0
  15. package/dist/conformance/beings.d.ts +114 -0
  16. package/dist/conformance/beings.js +126 -0
  17. package/dist/conformance/estate.d.ts +5 -0
  18. package/dist/conformance/estate.js +316 -0
  19. package/dist/conformance/index.d.ts +65 -0
  20. package/dist/conformance/index.js +448 -0
  21. package/dist/conformance/reach.d.ts +10 -0
  22. package/dist/conformance/reach.js +72 -0
  23. package/dist/conformance/store.d.ts +5 -0
  24. package/dist/conformance/store.js +97 -0
  25. package/dist/harbor/core.d.ts +46 -0
  26. package/dist/harbor/core.js +218 -0
  27. package/dist/harbor/dial.d.ts +8 -0
  28. package/dist/harbor/dial.js +45 -0
  29. package/dist/harbor/index.d.ts +6 -0
  30. package/dist/harbor/index.js +10 -0
  31. package/dist/harbor/memory.d.ts +24 -0
  32. package/dist/harbor/memory.js +68 -0
  33. package/dist/harbor/reach.d.ts +36 -0
  34. package/dist/harbor/reach.js +166 -0
  35. package/dist/harbor/store.d.ts +33 -0
  36. package/dist/harbor/store.js +41 -0
  37. package/dist/ward/allowance.d.ts +10 -0
  38. package/dist/ward/allowance.js +60 -0
  39. package/dist/ward/arithmetic.d.ts +26 -0
  40. package/dist/ward/arithmetic.js +159 -0
  41. package/dist/ward/cells.d.ts +3 -0
  42. package/dist/ward/cells.js +79 -0
  43. package/dist/ward/door.d.ts +16 -0
  44. package/dist/ward/door.js +127 -0
  45. package/dist/ward/ground.d.ts +15 -0
  46. package/dist/ward/ground.js +1 -0
  47. package/dist/ward/heirs.d.ts +14 -0
  48. package/dist/ward/heirs.js +102 -0
  49. package/dist/ward/index.d.ts +8 -0
  50. package/dist/ward/index.js +10 -0
  51. package/dist/ward/owner.d.ts +32 -0
  52. package/dist/ward/owner.js +116 -0
  53. package/dist/ward/partition.d.ts +57 -0
  54. package/dist/ward/partition.js +64 -0
  55. package/dist/ward/seal.d.ts +50 -0
  56. package/dist/ward/seal.js +109 -0
  57. package/dist/ward/stance.d.ts +19 -0
  58. package/dist/ward/stance.js +259 -0
  59. package/dist/ward/ward.d.ts +2 -0
  60. package/dist/ward/ward.js +197 -0
  61. package/package.json +21 -29
  62. package/src/being/index.ts +1 -1
  63. package/src/being/silence.ts +26 -8
  64. package/src/being/types.ts +13 -3
  65. package/src/conformance/beings.ts +5 -5
  66. package/src/conformance/estate.ts +11 -6
  67. package/src/conformance/index.ts +14 -12
  68. package/src/harbor/core.ts +56 -6
  69. package/src/harbor/dial.ts +1 -1
  70. package/src/harbor/memory.ts +6 -1
  71. package/src/ward/door.ts +54 -32
  72. package/src/ward/ground.ts +6 -2
  73. package/src/ward/heirs.ts +20 -5
  74. package/src/ward/owner.ts +4 -3
  75. package/src/ward/partition.ts +10 -0
  76. package/src/ward/seal.ts +8 -5
  77. package/src/ward/stance.ts +20 -14
  78. package/src/ward/ward.ts +9 -9
  79. package/vectors/framing.json +7 -1
@@ -0,0 +1,259 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The one stance builder. Used for every being the ward boots. Nothing
3
+ // outer is in what she holds: ids in, values out, keys in the bind table.
4
+ import { silence, isSilence, isUnreached, isWord, word } from '../being/silence.js';
5
+ import { digest } from '../being/digest.js';
6
+ import { RESERVED_IDS } from '../being/types.js';
7
+ import { at, put, drop } from './partition.js';
8
+ import { allow, within, LATE } from './allowance.js';
9
+ import { isWardPk } from './seal.js';
10
+ // The three calls on `standings` share the object with the ids she takes, so
11
+ // a standing named after one of them would be unreachable: `standings.knock`
12
+ // is the call, whatever record sits under that name. One namespace means one
13
+ // list: these are refused at invite and at take, beside the ward's own words.
14
+ const CALLS = ['knock', 'take', 'remove'];
15
+ const reserved = (id) => RESERVED_IDS.includes(id) || CALLS.includes(id);
16
+ export function buildStance(inside, key, cells, bind) {
17
+ // The name of one relation, as her side files it. A relation is a ward and
18
+ // a heir, never a heir alone: the heir pk is outer, it rides in the clear on
19
+ // every lid, and anyone who reads one can quote it back inside an invitation
20
+ // naming a ward of their own. Filed under the heir alone, that invitation
21
+ // would share her knock record with the relation the heir really belongs to:
22
+ // it would sign with that relation's key, and an answer to it would set the
23
+ // answered flag the other relation is taken on. Both halves, always.
24
+ const nameOf = (inv) => (inv.heir === undefined ? `public:${inv.ward}` : `${inv.ward}:${inv.heir}`);
25
+ const valid = (inv) => {
26
+ const i = inv;
27
+ if (!i || typeof i !== 'object' || !isWardPk(i.ward))
28
+ return false;
29
+ if (i.heir === undefined)
30
+ return i.secret === undefined;
31
+ return typeof i.heir === 'string' && typeof i.secret === 'string' && /^[0-9a-f]{64}$/.test(i.heir) && /^[0-9a-f]{64}$/.test(i.secret);
32
+ };
33
+ // One relation, one ask at a time. The rotation is a conversation: a send
34
+ // reads the key that speaks now, announces the next, and moves to it once
35
+ // the far door has answered. Two sends interleaving on one relation read
36
+ // each other's half-written keys and the two sides diverge, which no later
37
+ // ask can repair. So each relation has a lane, and a send waits for the one
38
+ // before it. Beings may still ask concurrently: the lanes are per relation,
39
+ // and a slow relation never holds up another.
40
+ const lanes = new Map();
41
+ const lane = (name, run) => {
42
+ const mine = (lanes.get(name) ?? Promise.resolve()).then(run, run);
43
+ const quiet = mine.then(() => { }, () => { });
44
+ lanes.set(name, quiet);
45
+ void quiet.then(() => {
46
+ if (lanes.get(name) === quiet)
47
+ lanes.delete(name); // the lane is idle. nothing to remember.
48
+ });
49
+ return mine;
50
+ };
51
+ // Every ask a being makes, bounded. The bound is outside the lane on
52
+ // purpose: an ask that waits its turn behind a relation that comes back
53
+ // round never reaches a send, and a bound that only watched the wire would
54
+ // never see it hang. A bound that ran out is silence, never unreached:
55
+ // unreached promises nothing was delivered and is safe to ask again, and a
56
+ // wait that gave up knows no such thing.
57
+ //
58
+ // The bell rings before the work is reached, too: an ask that was still
59
+ // waiting its turn when the wait ran out is never sent. Sent then, it would
60
+ // be a fresh ask nobody is waiting on, the far being would do the work, and
61
+ // what came back would be read into her cells behind a silence she was
62
+ // already handed. So the work is given the bell, and reads it at the lane.
63
+ const bounded = async (wanted, work) => {
64
+ let late = false;
65
+ const out = await within(allow(wanted).time, work(() => !late), () => {
66
+ late = true;
67
+ });
68
+ return out === LATE ? word('late') : out;
69
+ };
70
+ const send = async (keys, method, args, wanted, rec) => {
71
+ const r = await inside.send(bind, keys, true, method, args, wanted);
72
+ if (isSilence(r) || isWord(r))
73
+ return r;
74
+ if ('silence' in r)
75
+ return silence;
76
+ if ('quo' in r)
77
+ return word(r.quo); // the far door's word for her, sealed to her lid
78
+ if (rec && method !== undefined && r.seen !== null)
79
+ rec.seen = r.seen; // the digest rode along
80
+ return r.object;
81
+ };
82
+ const knocking = async (inv, method, args, wanted) => {
83
+ const name = nameOf(inv);
84
+ // her own key for this relation, minted once: rotation two. reused if she knocks again.
85
+ const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
86
+ // the first knock on a heir speaks as the heir, the key the inviter handed out, and announces her own.
87
+ // once bytes have come back, the heir is spent whatever they said, and her own key speaks.
88
+ // She sent once as the heir and nothing came back. Either the door heard,
89
+ // spent the heir and rotated to the key she announced, or nothing arrived
90
+ // and the heir still stands. Only the far door knows, and it will not say.
91
+ //
92
+ // So she asks, and the asking tells her. Under her own key: if the door
93
+ // heard, that key is the one it admits, and she is answered. If it did
94
+ // not, the key means nothing there, the ask is refused, and a refusal at
95
+ // that door writes nothing — the heir is untouched and still speaks. Then
96
+ // she knocks as the heir, as she would have. One extra round trip in the
97
+ // one case where a reply was lost, and the relation is not stranded.
98
+ //
99
+ // No stranger gains anything by it: whoever holds the invitation could
100
+ // always knock as the heir, and her own key is admitted only where the
101
+ // door already bound it to her.
102
+ if (inv.heir !== undefined && !k.spoke && k.sent) {
103
+ const own = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
104
+ const r = await inside.send(bind, own, true, method, args, wanted);
105
+ k.seq = own.seq;
106
+ k.current = own.current;
107
+ k.next = own.next;
108
+ if (isUnreached(r))
109
+ return r; // still nothing. she knows no more than before.
110
+ if (!isSilence(r) && !isWord(r) && 'object' in r) {
111
+ k.spoke = true; // it was heard the first time, and this is the answer
112
+ bind.answered[name] = true;
113
+ return r.object;
114
+ }
115
+ // Refused. The door never heard her, so the heir is still hers to spend.
116
+ }
117
+ const first = inv.heir !== undefined && !k.spoke;
118
+ const keys = 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 };
119
+ const r = await inside.send(bind, keys, true, method, args, wanted);
120
+ k.sent = true; // bytes went out. whether they arrived is the far door's to know.
121
+ // The number she spoke under, and the key she announced, kept whoever
122
+ // answered and kept when nobody did. A reply lost on the way back is a
123
+ // door that has already honoured the number: she must not offer it twice,
124
+ // or a road that drops one reply silences the relation for good.
125
+ k.seq = keys.seq;
126
+ if (!first) {
127
+ k.current = keys.current;
128
+ k.next = keys.next;
129
+ }
130
+ if (isUnreached(r))
131
+ return r; // nothing came back. she does not know whether it was heard.
132
+ // The heir is spent once the door honoured it, whatever she then said: an
133
+ // object, silence, or that she threw. A refusal wrote nothing, and says
134
+ // so by its word, so the heir still speaks and the next knock is as it.
135
+ k.spoke = !(!isSilence(r) && !isWord(r) && 'quo' in r && r.quo !== 'threw');
136
+ if (isSilence(r) || isWord(r))
137
+ return r;
138
+ if ('silence' in r)
139
+ return silence;
140
+ if ('quo' in r)
141
+ return word(r.quo);
142
+ bind.answered[name] = true;
143
+ return r.object;
144
+ };
145
+ // The id she took this relation under, if she took it. One relation is one
146
+ // ward and one heir whichever door it is reached by, so the standing and the
147
+ // invitation answer to the same name.
148
+ const keyName = (k) => (k.heir === null ? `public:${k.ward}` : `${k.ward}:${k.heir}`);
149
+ const takenAs = (name) => Object.keys(bind.standings).find((id) => keyName(bind.standings[id]) === name);
150
+ const calls = {
151
+ knock: async (inv, method, args = {}, wanted) => {
152
+ if (!valid(inv))
153
+ return word('invitation'); // S1. nothing is sent
154
+ // She may knock again, and after take that knock is an ask: the relation
155
+ // has one home, so it answers on the standing's lane, under the
156
+ // standing's keys and its count. A second lane here would speak for the
157
+ // same relation, and the two would refuse each other.
158
+ const id = takenAs(nameOf(inv));
159
+ if (id !== undefined)
160
+ return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, bind.standings[id], cells.standings[id], method, args, wanted) : Promise.resolve(word('late')))));
161
+ return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(word('late')))));
162
+ },
163
+ // Take consumes. Until now the relation lived in the knock record, under
164
+ // the ward and the heir; from now it lives in the standing, under her id.
165
+ // It never lives in both: two records seeded from one relation are two
166
+ // lanes speaking for one line of keys and one count, and whichever spoke
167
+ // last holds it while the other is refused. She would read that refusal as
168
+ // the far being falling quiet, which is a thing a sovereign being may do,
169
+ // so the loss would look exactly like an answer she is owed nothing of.
170
+ // One relation, one home, and the invitation is spent: knocking it again
171
+ // is another consumed it, and taking it again births nothing.
172
+ //
173
+ // So it is spoken on the relation's lane, like every other word spoken
174
+ // for it. A take reads the count and the keys a knock writes, and copies
175
+ // them into the standing. Off the lane it reads them between a knock's
176
+ // send and its answer: the standing is born holding a number the far door
177
+ // is about to honour and a key it is about to rotate past, and her first
178
+ // ask on it is refused. The relation she took correctly would be dead
179
+ // because a second knock on the same invitation was careless.
180
+ take: async (id, inv) => {
181
+ if (!valid(inv))
182
+ return null;
183
+ const name = nameOf(inv);
184
+ return lane(`knock:${name}`, () => {
185
+ if (!bind.answered[name])
186
+ return Promise.resolve(null);
187
+ // Standings and occupants are one namespace — invite and take each
188
+ // refuse a name the other holds — so a word reserved in one is
189
+ // reserved in both.
190
+ if (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))
191
+ return Promise.resolve(null);
192
+ const k = bind.knocks[name];
193
+ put(bind.standings, id, { ward: inv.ward, heir: inv.heir ?? null, current: k.current, next: k.next, seq: k.seq }); // the count carries over: the standing goes on where the knock left off
194
+ put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
195
+ delete bind.knocks[name];
196
+ delete bind.answered[name];
197
+ return Promise.resolve(id);
198
+ });
199
+ },
200
+ remove: (id) => {
201
+ drop(cells.standings, id);
202
+ drop(bind.standings, id);
203
+ },
204
+ };
205
+ const standingAt = (id) => ({
206
+ id,
207
+ // The keys are read now, not when the lane reaches this ask: an ask
208
+ // issued while the standing stood is answered even if she drops it in
209
+ // the next line. Waiting for the lane is not a reason to lose it.
210
+ ask: (method, args = {}, wanted) => {
211
+ const keys = at(bind.standings, id);
212
+ if (!keys)
213
+ return Promise.resolve(word('dropped')); // S2. she dropped it between one line and the next
214
+ const rec = at(cells.standings, id);
215
+ return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(word('late')))));
216
+ },
217
+ });
218
+ const asking = async (id, keys, rec, method, args, wanted) => {
219
+ const out = await send(keys, method, args, wanted, rec);
220
+ if (method === undefined && !isSilence(out) && !isWord(out)) {
221
+ const here = at(cells.standings, id);
222
+ if (here) {
223
+ here.blueprint = out;
224
+ here.digest = here.seen = await digest(out);
225
+ }
226
+ }
227
+ return out;
228
+ };
229
+ const standings = new Proxy(calls, {
230
+ // Own keys only: `in` would also find every name on Object's prototype,
231
+ // and a standing she took as `constructor` or `toString` would answer with
232
+ // Object's, not hers.
233
+ get: (t, id) => (typeof id === 'string' && Object.hasOwn(t, id) ? t[id] : typeof id === 'string' && at(cells.standings, id) ? standingAt(id) : undefined),
234
+ });
235
+ return {
236
+ cells,
237
+ occupants: {
238
+ // rotation one: the ward mints the occupant's first key and gives the secret away.
239
+ // it keeps the pk beside the id and nothing else. the invitation IS the key.
240
+ invite: async (id) => {
241
+ if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
242
+ return null;
243
+ put(cells.occupants, id, { id, notes: {} });
244
+ const k = await inside.mintKey(bind);
245
+ inside.openHeir(k.pk, key, id);
246
+ put(bind.occupants, id, k.pk);
247
+ return { ward: inside.pk, heir: k.pk, secret: k.seed };
248
+ },
249
+ remove: (id) => {
250
+ drop(cells.occupants, id);
251
+ const heir = at(bind.occupants, id);
252
+ drop(bind.occupants, id);
253
+ if (heir)
254
+ inside.closeHeir(heir); // the heir dies with the id
255
+ },
256
+ },
257
+ standings,
258
+ };
259
+ }
@@ -0,0 +1,2 @@
1
+ import type { Ground, WardPointers } from './ground.ts';
2
+ export declare function Ward(ground: Ground): Promise<WardPointers>;
@@ -0,0 +1,197 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A ward that is its own ward. Booted by a harbor with one object, the
3
+ // ground, and hands back two pointers: the door and the ask. Boots itself as
4
+ // the first being in its own map, and every being it later instantiates gets
5
+ // a stance built by the same code. Mints every key, seals every ask that
6
+ // leaves, opens every one that arrives.
7
+ import { silence, isSilence, unreached, isWord, word } from '../being/silence.js';
8
+ import { OWNER } from '../being/types.js';
9
+ import { at, open, put, emptyBind, emptyCells } from './partition.js';
10
+ import { Heirs } from './heirs.js';
11
+ import { makeDoor } from './door.js';
12
+ import { buildStance } from './stance.js';
13
+ import { ownerAnswer } from './owner.js';
14
+ import { allow, within, LATE } from './allowance.js';
15
+ import { beingKey, openReply, sealAsk, wardKey, wardPadlock, wardSignPk } from './seal.js';
16
+ import { unhex } from './arithmetic.js';
17
+ import { guardCells, cellFault } from './cells.js';
18
+ // How many minted pks she keeps behind her. Enough to name the keys in play.
19
+ const MINTED = 8;
20
+ // The ward. One call in, two pointers out. Async because keys are derived.
21
+ export async function Ward(ground) {
22
+ const w = await Self.boot(ground);
23
+ return { door: w.door, ask: w.ask };
24
+ }
25
+ class Self {
26
+ key;
27
+ pk;
28
+ g;
29
+ p;
30
+ heirs;
31
+ doors = new Map();
32
+ door;
33
+ ask;
34
+ static async boot(ground) {
35
+ return new Self(ground, await wardKey(ground.seed));
36
+ }
37
+ constructor(ground, key) {
38
+ this.g = ground;
39
+ this.key = key;
40
+ this.pk = key.pk;
41
+ this.p = open(ground.memory); // the partition IS the ward's cells
42
+ this.heirs = new Heirs(this.p);
43
+ this.door = makeDoor(key, this.heirs, this.doors, () => this.p.public, (n) => ground.random(n)); // pointer one
44
+ this.#boot(this.pk, this); // the ward's ward is itself
45
+ // a restart is silent: every being in the cells is constructed again, unasked.
46
+ // A throw on a boot the owner asked for is that boot's answer: silence,
47
+ // and nothing half-lives. This boot nobody asked for, and there is nobody
48
+ // to hand a silence to. Any constructor may throw — a default that is not
49
+ // a value, an author's mistake, a shape she no longer recognises after an
50
+ // upgrade — and let through here it is one being taking down the ward and
51
+ // every other being in it, on a restart. She is absent instead: no door,
52
+ // so silence at the door, and her cells sit untouched, waiting for the
53
+ // run that can read them.
54
+ for (const [k, c] of Object.entries(this.p.beings)) {
55
+ if (k === this.pk || !c.class)
56
+ continue;
57
+ try {
58
+ this.#instantiate(k, c.class);
59
+ }
60
+ catch {
61
+ /* she is not here this run. the ward is, and so is everyone else. */
62
+ }
63
+ }
64
+ // pointer two. the owner. the same answer every named asker gets, asker filled in as owner.
65
+ this.ask = async (method, args = {}) => {
66
+ try {
67
+ const out = await this.answer({ id: OWNER }, method, args);
68
+ return isSilence(out) || isWord(out) ? silence : out;
69
+ }
70
+ catch {
71
+ return silence;
72
+ }
73
+ };
74
+ }
75
+ // ---- the ward as a being. asked only by its owners: the root, through
76
+ // the ask pointer, and its occupants, through the door, whom only the root
77
+ // can invite. A stranger cannot be a named asker here, because the door
78
+ // names nobody the ward did not bind.
79
+ async answer(asker, method, args = {}) {
80
+ if (asker.id === undefined)
81
+ return { error: 'unknown ask' };
82
+ return ownerAnswer({
83
+ pk: this.pk,
84
+ doors: this.doors,
85
+ publicKey: () => this.p.public,
86
+ instantiate: (key, className, isPublic) => this.#instantiate(key, className, isPublic),
87
+ }, asker, method, args);
88
+ }
89
+ // ---- ward functions. on the object. no stance reaches them.
90
+ #instantiate(key, className, isPublic = false) {
91
+ if (!key || this.doors.has(key))
92
+ return null;
93
+ const door = this.#boot(key, className);
94
+ if (!door)
95
+ return null;
96
+ door.cells.class = className; // so a restart finds her
97
+ if (isPublic)
98
+ this.p.public = key;
99
+ return door;
100
+ }
101
+ // Nothing is written until there is somebody to write it for: a class the
102
+ // harbor does not know makes no being, and a boot that made nobody must
103
+ // leave the partition as it found it.
104
+ #boot(key, classNameOrSelf) {
105
+ const cells = guardCells(at(this.p.beings, key) ?? emptyCells());
106
+ const bind = at(this.p.bind, key) ?? emptyBind();
107
+ const stance = buildStance({
108
+ pk: this.pk,
109
+ mintKey: (b) => this.#mintKey(b),
110
+ openHeir: (heir, being, id) => this.heirs.open(heir, being, id),
111
+ closeHeir: (heir) => this.heirs.close(heir),
112
+ send: (b, keys, announce, method, args, wanted) => this.#send(b, keys, announce, method, args, wanted),
113
+ }, key, cells, bind);
114
+ const being = classNameOrSelf === this ? this : this.g.instantiate(classNameOrSelf, stance);
115
+ if (!being)
116
+ return null;
117
+ put(this.p.beings, key, cells);
118
+ put(this.p.bind, key, bind);
119
+ const door = { key, cells, bind, stance, being };
120
+ this.doors.set(key, door);
121
+ return door;
122
+ }
123
+ async #mintKey(bind) {
124
+ const k = await beingKey(this.g.random(32));
125
+ // The last few she minted, and no more. A relation rotates on every ask,
126
+ // so a list of all of them is a partition that grows for as long as she
127
+ // keeps talking and can never be written down. Nothing is read from this
128
+ // but the keys of the moment.
129
+ bind.minted.push(k.pk);
130
+ if (bind.minted.length > MINTED)
131
+ bind.minted.splice(0, bind.minted.length - MINTED);
132
+ return k;
133
+ }
134
+ // One send for every door, the ward's own included. Mine: never leaves.
135
+ // Not mine: carry. Signs as the standing's current key, announces its
136
+ // next, seals to the far ward, opens the reply with the ephemeral secret,
137
+ // and rotates to the announced key once the far door has answered under
138
+ // the current one.
139
+ async #send(bind, keys, announce, method, args, wanted) {
140
+ // What she asked for, held to what this ward allows. Asking for nothing is
141
+ // the default, and asking for more than the ceiling is the ceiling: budget
142
+ // is granted by a ward, never minted by a being.
143
+ const budget = allow(wanted);
144
+ const by = (await beingKey(unhex(keys.current))).pk;
145
+ // A public being is reached without a heir, and the door keeps nothing for
146
+ // whoever asked: no key it vouched for, so nothing to announce and nothing
147
+ // to rotate to. A standing on her signs with one key for life.
148
+ if (announce && keys.next === null && keys.heir !== null)
149
+ keys.next = (await this.#mintKey(bind)).seed;
150
+ const next = keys.next === null ? null : (await beingKey(unhex(keys.next))).pk;
151
+ // Her count for this relation, one higher every call and never reused. The
152
+ // far door honours each number once. One relation sends one at a time, so
153
+ // the numbers leave in the order they were taken.
154
+ const seq = (keys.seq += 1);
155
+ // Args that will not go through the seal are args that never left. A
156
+ // being holds three answers and a throw is not one of them, so an ask
157
+ // that cannot be sealed is told what is true of it: no door was reached.
158
+ // Values only, by the same rule her cells keep: a Date would arrive as a
159
+ // string, a function would not arrive at all, and she would never know
160
+ // the far being was asked something other than what she said.
161
+ if (cellFault(args, 'args') !== null)
162
+ return unreached();
163
+ let sealed;
164
+ try {
165
+ sealed = await sealAsk(keys.heir, { by, next, seq, time: budget.time, method, args }, unhex(keys.current), wardPadlock(keys.ward), this.g.random(32));
166
+ }
167
+ catch {
168
+ return unreached();
169
+ }
170
+ const { bytes, ephemeral } = sealed;
171
+ // The wait is bounded, and this is the one thing the ward times. A being
172
+ // holds three answers and a wait that does not end is none of them: a
173
+ // relation that comes back round holds a lane the answer needs, and only a
174
+ // bound on the wait can break that. What comes back late is not read.
175
+ //
176
+ // A wait that ran out is `late`, never unreached. Unreached promises
177
+ // nothing was delivered and is safe to retry; a bound that expired knows
178
+ // no such thing, because the far door may have heard and be working still.
179
+ //
180
+ // A harbor answers bytes or nothing. One that throws instead has answered
181
+ // nothing in a louder voice, and is read as nothing: no door was reached.
182
+ const carried = (keys.ward === this.pk ? this.door(new Uint8Array(bytes)).then((r) => r.bytes) : this.g.carry(keys.ward, bytes)).then((b) => b, () => undefined);
183
+ const out = await within(budget.time, carried);
184
+ if (out === LATE)
185
+ return word('late');
186
+ if (out === undefined)
187
+ return unreached(); // nothing came back. no door was reached.
188
+ const reply = await openReply(out, ephemeral, wardSignPk(keys.ward));
189
+ if (!reply)
190
+ return silence; // did not open, or not from that ward: not Quo's bytes
191
+ if ('object' in reply && keys.next !== null) {
192
+ keys.current = keys.next; // the far door holds `next` as announced. move to it.
193
+ keys.next = null;
194
+ }
195
+ return reply;
196
+ }
197
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/quo",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
5
5
  "keywords": [
6
6
  "quo",
@@ -18,42 +18,34 @@
18
18
  "node": ">=22.18"
19
19
  },
20
20
  "exports": {
21
- ".": "./src/being/index.ts",
22
- "./ward": "./src/ward/index.ts",
23
- "./harbor": "./src/harbor/index.ts",
24
- "./conformance": "./src/conformance/index.ts"
21
+ ".": {
22
+ "types": "./dist/being/index.d.ts",
23
+ "default": "./dist/being/index.js"
24
+ },
25
+ "./ward": {
26
+ "types": "./dist/ward/index.d.ts",
27
+ "default": "./dist/ward/index.js"
28
+ },
29
+ "./harbor": {
30
+ "types": "./dist/harbor/index.d.ts",
31
+ "default": "./dist/harbor/index.js"
32
+ },
33
+ "./conformance": {
34
+ "types": "./dist/conformance/index.d.ts",
35
+ "default": "./dist/conformance/index.js"
36
+ }
25
37
  },
26
38
  "scripts": {
27
- "check": "npm run typecheck && npm run lint && npm test && npm run check:estate",
28
- "check:estate": "node --test \"estate/test/*.test.ts\"",
29
- "typecheck": "tsc",
30
- "typecheck:watch": "tsc --watch",
31
- "lint": "npm run lint:ts && npm run lint:md",
32
- "lint:ts": "oxlint --type-aware",
33
- "lint:md": "markdownlint-cli2",
34
- "lint:fix": "oxlint --type-aware --fix && markdownlint-cli2 --fix",
39
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
35
40
  "test": "node --test \"test/*.test.ts\"",
36
- "check:terrain": "node --test \"test/terrain/*.test.ts\" \"estate/test/terrain/*.test.ts\"",
37
- "test:being": "node --test test/being.test.ts",
38
- "test:ward": "node --test test/ward.test.ts",
39
- "prepublishOnly": "npm run check && npm run check:terrain"
40
- },
41
- "devDependencies": {
42
- "@types/node": "^26.4.1",
43
- "bun": "^1.4.0",
44
- "deno": "^2.9.6",
45
- "esbuild": "^0.28.2",
46
- "markdownlint-cli2": "^0.23.2",
47
- "oxlint": "^1.81.0",
48
- "oxlint-tsgolint": "^7.0.2001",
49
- "playwright": "^1.62.1",
50
- "typescript": "^7.0.2",
51
- "workerd": "^1.20260904.1"
41
+ "check:terrain": "node --test \"test/terrain/*.test.ts\"",
42
+ "prepublishOnly": "cd ../.. && npm run check && npm run check:terrain"
52
43
  },
53
44
  "publishConfig": {
54
45
  "access": "public"
55
46
  },
56
47
  "files": [
48
+ "dist",
57
49
  "src",
58
50
  "vectors",
59
51
  "SPEC.md",
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // @quo-systems/quo — the Being side. What a being author imports, if anything.
3
3
  export { Being, type AskSpec } from './being.ts';
4
- export { silence, isSilence, unreached, isUnreached } from './silence.ts';
4
+ export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.ts';
5
5
  export { digest, canonical } from './digest.ts';
6
6
  export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
7
7
  export type * from './types.ts';
@@ -1,14 +1,32 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Silence is one distinguished value. Null is an answer. Silence is no answer.
3
- import { UNREACHED_KEY, type Unreached } from './types.ts';
3
+ //
4
+ // The ward's words are the other distinguished values: what her ward tells
5
+ // her when no object came back and it knows why. Each is one frozen object
6
+ // under one symbol key, so a being cannot make one by accident, none of them
7
+ // crosses an edge as a value, and a being who compares against them needs
8
+ // no fourth shape. `unreached` is one of them.
9
+ import { WORD_KEY, type DoorWord, type Unreached, type Word, type WordName } from './types.ts';
4
10
 
5
11
  export const silence: unique symbol = Symbol.for('quo.silence');
6
12
  export const isSilence = (x: unknown): x is typeof silence => x === silence;
7
13
 
8
- // Unreached is the ward's own word: no far door was reached. It carries no
9
- // reason. Safe to retry, because nothing was delivered. A being cannot
10
- // produce it: a ward that sees it come out of a being treats it as silence.
11
- const UNREACHED: Unreached = Object.freeze({ [UNREACHED_KEY]: true });
12
- export const unreached = (): Unreached => UNREACHED;
13
- export const isUnreached = (x: unknown): x is Unreached =>
14
- x !== null && typeof x === 'object' && (x as Record<symbol, unknown>)[UNREACHED_KEY] === true;
14
+ // The words a door says to a key it has bound, and no other. They ride on
15
+ // the wire as `{ quo: word }`, so a reply is checked against this list.
16
+ export const DOOR_WORDS: readonly DoorWord[] = ['removed', 'absent', 'unannounced', 'repeated', 'threw'];
17
+ export const isDoorWord = (s: unknown): s is DoorWord => typeof s === 'string' && (DOOR_WORDS as readonly string[]).includes(s);
18
+
19
+ const WORDS = new Map<WordName, Word>();
20
+ export const word = <W extends WordName>(name: W): Word<W> => {
21
+ let w = WORDS.get(name);
22
+ if (!w) WORDS.set(name, (w = Object.freeze({ [WORD_KEY]: name })));
23
+ return w as Word<W>;
24
+ };
25
+ export const isWord = (x: unknown): x is Word => x !== null && typeof x === 'object' && typeof (x as Record<symbol, unknown>)[WORD_KEY] === 'string';
26
+ export const wordOf = (x: Word): WordName => x[WORD_KEY];
27
+
28
+ // Unreached: no far door was reached. Nothing is known to have been
29
+ // delivered, so asking again is safe. A being cannot produce it: a ward that
30
+ // sees a word come out of a being reads it as her having thrown.
31
+ export const unreached = (): Unreached => word('unreached');
32
+ export const isUnreached = (x: unknown): x is Unreached => isWord(x) && wordOf(x) === 'unreached';
@@ -44,11 +44,21 @@ export type Cells = {
44
44
  };
45
45
 
46
46
  export type Silence = typeof silence;
47
- export type Unreached = { readonly [K in typeof UNREACHED_KEY]: true };
48
- export const UNREACHED_KEY: unique symbol = Symbol.for('quo.unreached');
47
+
48
+ // The ward's words: what her ward says when no object came back and it knows
49
+ // why. A door says the first five to a key it has bound, and rides them on
50
+ // the wire as `{ quo: word }`; her own ward says the last four to her and
51
+ // they never leave the ward. Each is one frozen object under one symbol key,
52
+ // so no being makes one and none crosses an edge as a value.
53
+ export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
54
+ export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
55
+ export type WordName = DoorWord | WardWord;
56
+ export const WORD_KEY: unique symbol = Symbol.for('quo.word');
57
+ export type Word<W extends WordName = WordName> = { readonly [K in typeof WORD_KEY]: W };
58
+ export type Unreached = Word<'unreached'>;
49
59
 
50
60
  // What comes back from an ask or a knock.
51
- export type Answer = Json | Silence | Unreached;
61
+ export type Answer = Json | Silence | Word;
52
62
  // What a being answers.
53
63
  export type Reply = Json | Silence;
54
64
 
@@ -2,7 +2,7 @@
2
2
  // The fixture beings. Fixed, so that the ward is what varies. A printer, a
3
3
  // shop, a customer on the base class, and one raw being with no base at all.
4
4
  import { Being } from '../being/being.ts';
5
- import { isSilence, isUnreached } from '../being/silence.ts';
5
+ import { isSilence, isUnreached, isWord } from '../being/silence.ts';
6
6
  import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
7
7
 
8
8
  // A printer. Invites whom she is told to, prints for her occupants. Her
@@ -46,7 +46,7 @@ export class Shop extends Being {
46
46
 
47
47
  async keepPrinter(invitation: Invitation): Promise<boolean> {
48
48
  const bp = await this.knock(invitation);
49
- if (isSilence(bp) || isUnreached(bp)) return false;
49
+ if (isSilence(bp) || isWord(bp)) return false;
50
50
  this.cells.printerId = await this.take('prn', invitation);
51
51
  return true;
52
52
  }
@@ -55,7 +55,7 @@ export class Shop extends Being {
55
55
  const rec = this.occupant(asker);
56
56
  if (rec?.notes.tier === 'gold' && invitation) {
57
57
  const back = await this.knock(invitation as Invitation, 'hi');
58
- if (!isSilence(back) && !isUnreached(back)) await this.take(`vip-${asker.id}`, invitation as Invitation);
58
+ if (!isSilence(back) && !isWord(back)) await this.take(`vip-${asker.id}`, invitation as Invitation);
59
59
  }
60
60
  return { welcome: true };
61
61
  }
@@ -67,7 +67,7 @@ export class Shop extends Being {
67
67
  if (!printer) return { error: 'no printer' };
68
68
  const out = await printer.ask('print', { doc: `receipt for ${item}` });
69
69
  if (isUnreached(out)) return { error: 'printer unreachable', retry: true };
70
- if (isSilence(out)) return { error: 'printer refused' };
70
+ if (isSilence(out) || isWord(out)) return { error: 'printer refused' };
71
71
  const st = this.cells.standings[id!];
72
72
  if (st.seen !== st.digest) this.occupant(asker)!.notes.printerChanged = true;
73
73
  (this.cells.sales as JsonObject[]).push({ item: item ?? null, by: asker.id ?? null });
@@ -88,7 +88,7 @@ export class Customer extends Being {
88
88
  async join(invitation: Invitation) {
89
89
  const mine = (await this.invite('shop-back'))!;
90
90
  const out = await this.knock(invitation, 'hello', { invitation: mine as never });
91
- if (isSilence(out) || isUnreached(out)) return out; // nothing was born
91
+ if (isSilence(out) || isWord(out)) return out; // nothing was born
92
92
  await this.take('shop', invitation); // now, and only now
93
93
  return out;
94
94
  }