@quo-systems/quo 0.1.0 → 0.1.1

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 (63) hide show
  1. package/README.md +9 -6
  2. package/SPEC.md +51 -24
  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 +5 -0
  10. package/dist/being/silence.js +11 -0
  11. package/dist/being/types.d.ts +80 -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 +310 -0
  19. package/dist/conformance/index.d.ts +65 -0
  20. package/dist/conformance/index.js +446 -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 +41 -0
  26. package/dist/harbor/core.js +209 -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 +20 -0
  32. package/dist/harbor/memory.js +63 -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 +12 -0
  44. package/dist/ward/door.js +107 -0
  45. package/dist/ward/ground.d.ts +12 -0
  46. package/dist/ward/ground.js +1 -0
  47. package/dist/ward/heirs.d.ts +12 -0
  48. package/dist/ward/heirs.js +85 -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 +114 -0
  53. package/dist/ward/partition.d.ts +51 -0
  54. package/dist/ward/partition.js +62 -0
  55. package/dist/ward/seal.d.ts +48 -0
  56. package/dist/ward/seal.js +105 -0
  57. package/dist/ward/stance.d.ts +19 -0
  58. package/dist/ward/stance.js +250 -0
  59. package/dist/ward/ward.d.ts +2 -0
  60. package/dist/ward/ward.js +197 -0
  61. package/package.json +10 -6
  62. package/src/harbor/core.ts +45 -4
  63. package/src/harbor/dial.ts +1 -1
@@ -0,0 +1,250 @@
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 } 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 ? silence : 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) || isUnreached(r))
73
+ return r;
74
+ if ('silence' in r)
75
+ return silence;
76
+ if (rec && method !== undefined && r.seen !== null)
77
+ rec.seen = r.seen; // the digest rode along
78
+ return r.object;
79
+ };
80
+ const knocking = async (inv, method, args, wanted) => {
81
+ const name = nameOf(inv);
82
+ // her own key for this relation, minted once: rotation two. reused if she knocks again.
83
+ const k = (bind.knocks[name] ??= { current: (await inside.mintKey(bind)).seed, next: null, spoke: false, sent: false, seq: 0 });
84
+ // the first knock on a heir speaks as the heir, the key the inviter handed out, and announces her own.
85
+ // once bytes have come back, the heir is spent whatever they said, and her own key speaks.
86
+ // She sent once as the heir and nothing came back. Either the door heard,
87
+ // spent the heir and rotated to the key she announced, or nothing arrived
88
+ // and the heir still stands. Only the far door knows, and it will not say.
89
+ //
90
+ // So she asks, and the asking tells her. Under her own key: if the door
91
+ // heard, that key is the one it admits, and she is answered. If it did
92
+ // not, the key means nothing there, the ask is refused, and a refusal at
93
+ // that door writes nothing — the heir is untouched and still speaks. Then
94
+ // she knocks as the heir, as she would have. One extra round trip in the
95
+ // one case where a reply was lost, and the relation is not stranded.
96
+ //
97
+ // No stranger gains anything by it: whoever holds the invitation could
98
+ // always knock as the heir, and her own key is admitted only where the
99
+ // door already bound it to her.
100
+ if (inv.heir !== undefined && !k.spoke && k.sent) {
101
+ const own = { ward: inv.ward, heir: inv.heir, current: k.current, next: k.next, seq: k.seq };
102
+ const r = await inside.send(bind, own, true, method, args, wanted);
103
+ k.seq = own.seq;
104
+ k.current = own.current;
105
+ k.next = own.next;
106
+ if (isUnreached(r))
107
+ return r; // still nothing. she knows no more than before.
108
+ if (!isSilence(r) && !('silence' in r)) {
109
+ k.spoke = true; // it was heard the first time, and this is the answer
110
+ bind.answered[name] = true;
111
+ return r.object;
112
+ }
113
+ // Refused. The door never heard her, so the heir is still hers to spend.
114
+ }
115
+ const first = inv.heir !== undefined && !k.spoke;
116
+ 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 };
117
+ const r = await inside.send(bind, keys, true, method, args, wanted);
118
+ k.sent = true; // bytes went out. whether they arrived is the far door's to know.
119
+ // The number she spoke under, and the key she announced, kept whoever
120
+ // answered and kept when nobody did. A reply lost on the way back is a
121
+ // door that has already honoured the number: she must not offer it twice,
122
+ // or a road that drops one reply silences the relation for good.
123
+ k.seq = keys.seq;
124
+ if (!first) {
125
+ k.current = keys.current;
126
+ k.next = keys.next;
127
+ }
128
+ if (isUnreached(r))
129
+ return r; // nothing came back. she does not know whether it was heard.
130
+ k.spoke = true;
131
+ if (isSilence(r) || 'silence' in r)
132
+ return silence;
133
+ bind.answered[name] = true;
134
+ return r.object;
135
+ };
136
+ // The id she took this relation under, if she took it. One relation is one
137
+ // ward and one heir whichever door it is reached by, so the standing and the
138
+ // invitation answer to the same name.
139
+ const keyName = (k) => (k.heir === null ? `public:${k.ward}` : `${k.ward}:${k.heir}`);
140
+ const takenAs = (name) => Object.keys(bind.standings).find((id) => keyName(bind.standings[id]) === name);
141
+ const calls = {
142
+ knock: async (inv, method, args = {}, wanted) => {
143
+ if (!valid(inv))
144
+ return silence;
145
+ // She may knock again, and after take that knock is an ask: the relation
146
+ // has one home, so it answers on the standing's lane, under the
147
+ // standing's keys and its count. A second lane here would speak for the
148
+ // same relation, and the two would refuse each other.
149
+ const id = takenAs(nameOf(inv));
150
+ if (id !== undefined)
151
+ return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, bind.standings[id], cells.standings[id], method, args, wanted) : Promise.resolve(silence))));
152
+ return bounded(wanted, (live) => lane(`knock:${nameOf(inv)}`, () => (live() ? knocking(inv, method, args, wanted) : Promise.resolve(silence))));
153
+ },
154
+ // Take consumes. Until now the relation lived in the knock record, under
155
+ // the ward and the heir; from now it lives in the standing, under her id.
156
+ // It never lives in both: two records seeded from one relation are two
157
+ // lanes speaking for one line of keys and one count, and whichever spoke
158
+ // last holds it while the other is refused. She would read that refusal as
159
+ // the far being falling quiet, which is a thing a sovereign being may do,
160
+ // so the loss would look exactly like an answer she is owed nothing of.
161
+ // One relation, one home, and the invitation is spent: knocking it again
162
+ // is another consumed it, and taking it again births nothing.
163
+ //
164
+ // So it is spoken on the relation's lane, like every other word spoken
165
+ // for it. A take reads the count and the keys a knock writes, and copies
166
+ // them into the standing. Off the lane it reads them between a knock's
167
+ // send and its answer: the standing is born holding a number the far door
168
+ // is about to honour and a key it is about to rotate past, and her first
169
+ // ask on it is refused. The relation she took correctly would be dead
170
+ // because a second knock on the same invitation was careless.
171
+ take: async (id, inv) => {
172
+ if (!valid(inv))
173
+ return null;
174
+ const name = nameOf(inv);
175
+ return lane(`knock:${name}`, () => {
176
+ if (!bind.answered[name])
177
+ return Promise.resolve(null);
178
+ // Standings and occupants are one namespace — invite and take each
179
+ // refuse a name the other holds — so a word reserved in one is
180
+ // reserved in both.
181
+ if (reserved(id) || at(cells.standings, id) || at(cells.occupants, id))
182
+ return Promise.resolve(null);
183
+ const k = bind.knocks[name];
184
+ 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
185
+ put(cells.standings, id, { id, digest: null, blueprint: null, seen: null });
186
+ delete bind.knocks[name];
187
+ delete bind.answered[name];
188
+ return Promise.resolve(id);
189
+ });
190
+ },
191
+ remove: (id) => {
192
+ drop(cells.standings, id);
193
+ drop(bind.standings, id);
194
+ },
195
+ };
196
+ const standingAt = (id) => ({
197
+ id,
198
+ // The keys are read now, not when the lane reaches this ask: an ask
199
+ // issued while the standing stood is answered even if she drops it in
200
+ // the next line. Waiting for the lane is not a reason to lose it.
201
+ ask: (method, args = {}, wanted) => {
202
+ const keys = at(bind.standings, id);
203
+ if (!keys)
204
+ return Promise.resolve(silence); // she dropped it between one line and the next
205
+ const rec = at(cells.standings, id);
206
+ return bounded(wanted, (live) => lane(`ask:${id}`, () => (live() ? asking(id, keys, rec, method, args, wanted) : Promise.resolve(silence))));
207
+ },
208
+ });
209
+ const asking = async (id, keys, rec, method, args, wanted) => {
210
+ const out = await send(keys, method, args, wanted, rec);
211
+ if (method === undefined && !isSilence(out) && !isUnreached(out)) {
212
+ const here = at(cells.standings, id);
213
+ if (here) {
214
+ here.blueprint = out;
215
+ here.digest = here.seen = await digest(out);
216
+ }
217
+ }
218
+ return out;
219
+ };
220
+ const standings = new Proxy(calls, {
221
+ // Own keys only: `in` would also find every name on Object's prototype,
222
+ // and a standing she took as `constructor` or `toString` would answer with
223
+ // Object's, not hers.
224
+ get: (t, id) => (typeof id === 'string' && Object.hasOwn(t, id) ? t[id] : typeof id === 'string' && at(cells.standings, id) ? standingAt(id) : undefined),
225
+ });
226
+ return {
227
+ cells,
228
+ occupants: {
229
+ // rotation one: the ward mints the occupant's first key and gives the secret away.
230
+ // it keeps the pk beside the id and nothing else. the invitation IS the key.
231
+ invite: async (id) => {
232
+ if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id))
233
+ return null;
234
+ put(cells.occupants, id, { id, notes: {} });
235
+ const k = await inside.mintKey(bind);
236
+ inside.openHeir(k.pk, key, id);
237
+ put(bind.occupants, id, k.pk);
238
+ return { ward: inside.pk, heir: k.pk, secret: k.seed };
239
+ },
240
+ remove: (id) => {
241
+ drop(cells.occupants, id);
242
+ const heir = at(bind.occupants, id);
243
+ drop(bind.occupants, id);
244
+ if (heir)
245
+ inside.closeHeir(heir); // the heir dies with the id
246
+ },
247
+ },
248
+ standings,
249
+ };
250
+ }
@@ -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, isUnreached } 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) || isUnreached(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 silence, 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)) : this.g.carry(keys.ward, bytes)).then((b) => b, () => undefined);
183
+ const out = await within(budget.time, carried);
184
+ if (out === LATE)
185
+ return silence;
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
191
+ if (!('silence' 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.1.1",
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,20 +18,23 @@
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
+ ".": { "types": "./dist/being/index.d.ts", "default": "./dist/being/index.js" },
22
+ "./ward": { "types": "./dist/ward/index.d.ts", "default": "./dist/ward/index.js" },
23
+ "./harbor": { "types": "./dist/harbor/index.d.ts", "default": "./dist/harbor/index.js" },
24
+ "./conformance": { "types": "./dist/conformance/index.d.ts", "default": "./dist/conformance/index.js" }
25
25
  },
26
26
  "scripts": {
27
- "check": "npm run typecheck && npm run lint && npm test && npm run check:estate",
27
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
28
+ "check": "npm run build && npm run typecheck && npm run lint && npm test && npm run check:estate",
28
29
  "check:estate": "node --test \"estate/test/*.test.ts\"",
30
+ "pretypecheck": "npm run build",
29
31
  "typecheck": "tsc",
30
32
  "typecheck:watch": "tsc --watch",
31
33
  "lint": "npm run lint:ts && npm run lint:md",
32
34
  "lint:ts": "oxlint --type-aware",
33
35
  "lint:md": "markdownlint-cli2",
34
36
  "lint:fix": "oxlint --type-aware --fix && markdownlint-cli2 --fix",
37
+ "pretest": "npm run build",
35
38
  "test": "node --test \"test/*.test.ts\"",
36
39
  "check:terrain": "node --test \"test/terrain/*.test.ts\" \"estate/test/terrain/*.test.ts\"",
37
40
  "test:being": "node --test test/being.test.ts",
@@ -54,6 +57,7 @@
54
57
  "access": "public"
55
58
  },
56
59
  "files": [
60
+ "dist",
57
61
  "src",
58
62
  "vectors",
59
63
  "SPEC.md",
@@ -9,8 +9,9 @@
9
9
  // tree, and every one of them passes the conformance suite untouched.
10
10
  //
11
11
  // The directory is filled three ways, in this order: the harbor's own
12
- // doors; a socket a dialer holds to it, bound at announce and unbound at
13
- // close; a hint from a link. And one fallback: a dialer with nothing in its
12
+ // doors; a socket a dialer holds to it, bound once the door behind the
13
+ // dialer's claim has proved it holds the key, and unbound at close; a hint
14
+ // from a link. And one fallback: a dialer with nothing in its
14
15
  // directory for a pk sends down the socket it holds, because the listener
15
16
  // it dialed is the rendezvous and may hold that pk on another socket. Bytes
16
17
  // that arrive from the wire go to an own door or a held socket and never
@@ -19,6 +20,9 @@ import { Ward } from '../ward/ward.ts';
19
20
  import type { Ground, WardPointers } from '../ward/ground.ts';
20
21
  import type { BeingClass, BeingLike } from '../being/types.ts';
21
22
  import { request, type Reach } from './reach.ts';
23
+ import { openReply, wardSignPk } from '../ward/seal.ts';
24
+ import { concat, sealingPair, KEY } from '../ward/arithmetic.ts';
25
+ import { within, LATE } from '../ward/allowance.ts';
22
26
  import type { Kept, Store, WardRecord } from './store.ts';
23
27
 
24
28
  export type Hosted = WardPointers & { pk: string; name: string; record: WardRecord; partition: Record<string, unknown>; being(key: string): BeingLike | undefined; save(): Promise<void> };
@@ -30,6 +34,9 @@ export type Bound = { reach: Reach; held: boolean };
30
34
  export type Loader = (record: WardRecord) => Promise<Record<string, BeingClass>>;
31
35
 
32
36
  export const DEFAULT_CODE = 'classes/index.ts';
37
+ // How long a probe waits for the door behind a claim. A claim that answers
38
+ // nothing in this time is not bound; the next announce is another chance.
39
+ const PROBE = 10_000;
33
40
 
34
41
  export class Harbor {
35
42
  readonly store: Store;
@@ -95,8 +102,42 @@ export class Harbor {
95
102
  await this.store.hint(pk, url);
96
103
  }
97
104
 
98
- bind(pks: string[], reach: Reach, held: boolean): void {
99
- for (const pk of pks) if (!this.doors.has(pk)) this.reaches.set(pk, { reach, held });
105
+ // An announce is a claim, and a claim binds nothing until proven: anyone
106
+ // who can reach a listener could otherwise name a pk that is not theirs and
107
+ // take its reachability. The proof is the door itself, as it already is. A
108
+ // lid this harbor minted and noise after it is a box that does not open,
109
+ // and the door answers one such box with silence sealed to the lid and
110
+ // signed by the ward key. Only the holder of that ward's seed can write
111
+ // that reply, the lid is fresh so nothing replays, and a box that does not
112
+ // open writes nothing at the ward. This is the one box a harbor ever opens,
113
+ // the one it sealed itself, and it reads nothing from it but that the
114
+ // signature is the claimed key's.
115
+ async prove(pk: string, reach: Reach): Promise<boolean> {
116
+ if (!/^[0-9a-f]{128}$/.test(pk)) return false;
117
+ try {
118
+ const random = (n: number) => globalThis.crypto.getRandomValues(new Uint8Array(n));
119
+ const lid = await sealingPair(random(KEY));
120
+ const back = await within(PROBE, reach.carry(pk, concat([lid.pk, random(KEY)])));
121
+ if (back === LATE || back === undefined) return false;
122
+ const reply = await openReply(back, lid.secret, wardSignPk(pk));
123
+ return reply !== null && 'silence' in reply;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ // Bind what the far side claims, each pk proven at its door first. Both
130
+ // sides bind this way: the listener the dialer's claims, the dialer the
131
+ // listener's, since a claim is a claim whichever end made it.
132
+ async bind(pks: string[], reach: Reach, held: boolean): Promise<void> {
133
+ // An announce is the whole of what that side holds now, so a pk this
134
+ // reach was bound for and no longer claims is unbound at once: a ward
135
+ // that left a dialer is not reachable through it, proof or no proof.
136
+ for (const [pk, b] of this.reaches) if (b.reach === reach && !pks.includes(pk)) this.reaches.delete(pk);
137
+ await Promise.all(pks.map(async (pk) => {
138
+ if (this.doors.has(pk) || !(await this.prove(pk, reach))) return;
139
+ this.reaches.set(pk, { reach, held });
140
+ }));
100
141
  }
101
142
  unbind(reach: Reach): void {
102
143
  for (const [pk, b] of this.reaches) if (b.reach === reach) this.reaches.delete(pk);
@@ -30,7 +30,7 @@ export function dial(harbor: Harbor, url: string): Dialer {
30
30
  // not at the handshake: a listener that opens and then refuses every
31
31
  // time would otherwise be dialed at a fixed two seconds for good.
32
32
  wait = 1000;
33
- harbor.bind(far, s, false); // the listener's pks: reached through this socket, not held for others
33
+ void harbor.bind(far, s, false); // the listener's claims, each proven at its door: reached through this socket, not held for others
34
34
  },
35
35
  (why) => {
36
36
  harbor.unbind(s);