@quo-systems/quo 0.2.14 → 0.2.16

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 (62) hide show
  1. package/README.md +37 -20
  2. package/dist/being/being.d.ts +1 -0
  3. package/dist/being/being.js +8 -3
  4. package/dist/being/index.d.ts +1 -1
  5. package/dist/being/index.js +1 -1
  6. package/dist/being/types.d.ts +2 -0
  7. package/dist/being/types.js +17 -0
  8. package/dist/conformance/beings.d.ts +41 -0
  9. package/dist/conformance/beings.js +28 -2
  10. package/dist/conformance/index.d.ts +10 -1
  11. package/dist/conformance/index.js +162 -6
  12. package/dist/harbor/core.d.ts +4 -2
  13. package/dist/harbor/core.js +25 -4
  14. package/dist/harbor/index.d.ts +1 -1
  15. package/dist/harbor/memory.d.ts +4 -3
  16. package/dist/harbor/memory.js +13 -4
  17. package/dist/harbor/reach.js +1 -1
  18. package/dist/ward/arithmetic.d.ts +4 -0
  19. package/dist/ward/arithmetic.js +82 -12
  20. package/dist/ward/cells.d.ts +2 -0
  21. package/dist/ward/cells.js +60 -11
  22. package/dist/ward/door.d.ts +1 -0
  23. package/dist/ward/door.js +29 -10
  24. package/dist/ward/ground.d.ts +3 -1
  25. package/dist/ward/ground.js +1 -1
  26. package/dist/ward/heirs.js +8 -1
  27. package/dist/ward/index.d.ts +2 -2
  28. package/dist/ward/index.js +3 -3
  29. package/dist/ward/owner.js +49 -8
  30. package/dist/ward/seal.d.ts +1 -0
  31. package/dist/ward/seal.js +11 -3
  32. package/dist/ward/stance.d.ts +1 -0
  33. package/dist/ward/stance.js +62 -4
  34. package/dist/ward/ward.js +13 -4
  35. package/package.json +7 -6
  36. package/{SPEC.md → protocol/SPEC.md} +367 -537
  37. package/protocol/vectors/door.json +345 -0
  38. package/quo-kit.md +595 -0
  39. package/src/being/being.ts +8 -3
  40. package/src/being/index.ts +1 -1
  41. package/src/being/types.ts +34 -0
  42. package/src/conformance/beings.ts +25 -2
  43. package/src/conformance/estate.ts +9 -9
  44. package/src/conformance/index.ts +204 -7
  45. package/src/conformance/reach.ts +1 -1
  46. package/src/harbor/core.ts +26 -5
  47. package/src/harbor/index.ts +1 -1
  48. package/src/harbor/memory.ts +14 -5
  49. package/src/harbor/reach.ts +1 -1
  50. package/src/ward/arithmetic.ts +83 -14
  51. package/src/ward/cells.ts +59 -10
  52. package/src/ward/door.ts +27 -9
  53. package/src/ward/ground.ts +39 -11
  54. package/src/ward/heirs.ts +7 -1
  55. package/src/ward/index.ts +4 -4
  56. package/src/ward/owner.ts +45 -10
  57. package/src/ward/seal.ts +12 -3
  58. package/src/ward/stance.ts +60 -4
  59. package/src/ward/ward.ts +14 -5
  60. /package/{vectors → protocol/vectors}/arithmetic.json +0 -0
  61. /package/{vectors → protocol/vectors}/framing.json +0 -0
  62. /package/{vectors → protocol/vectors}/wire.json +0 -0
@@ -1,14 +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, isWord, word, wordOf } from '../being/silence.ts';
4
+ import { silence, isSilence, isWord, unreached, word, wordOf } from '../being/silence.ts';
5
5
  import { digest } from '../being/digest.ts';
6
6
  import { RESERVED_IDS, isBlueprint } 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, prune, type Bind, type StandingKeys } from './partition.ts';
9
9
  import { allow, within, LATE } from './allowance.ts';
10
10
  import { isHex, isWardPk, type ReplyPayload } from './seal.ts';
11
- import { cellFault } from './cells.ts';
11
+ import { cellFault, unkeepable } from './cells.ts';
12
12
 
13
13
  // What the stance needs from its ward, and no more.
14
14
  export type Inside = {
@@ -29,6 +29,11 @@ export type Inside = {
29
29
  instantiate(className: string, key: string): string | null;
30
30
  // mint an occupant on a being of this ward, for the being who made her.
31
31
  relate(key: string, id: string): Promise<Invitation | null>;
32
+ // an invitation to one of the harbor's own beings, by the harbor's name for
33
+ // it, handed to the taker and never returned. false where this box lends
34
+ // nothing of that name to this ward, or where the taker did not take it, in
35
+ // which case the harbor has already removed what it minted.
36
+ lend(name: string, take: (invitation: Invitation) => Promise<boolean>): Promise<boolean>;
32
37
  // take a being of this ward out again, with every relation she holds.
33
38
  unmake(key: string): void;
34
39
  // the bind table was written: a record put, dropped or moved on.
@@ -43,9 +48,22 @@ export type SealedInvitation = { ward: string; heir?: string; secret?: string };
43
48
  // is the call, whatever record sits under that name. One namespace means one
44
49
  // list: these are refused at invite and at take, beside the ward's own words.
45
50
  const CALLS = ['knock', 'take', 'remove'];
46
- const reserved = (id: string): boolean => RESERVED_IDS.includes(id) || CALLS.includes(id);
47
51
 
48
52
  export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bind): Stance {
53
+ // A word the ward refuses at every mint and every take. The two the
54
+ // protocol names, and the ones this kit adds because its own spelling took
55
+ // them: the calls a standing already answers to, and a name no partition
56
+ // can hold. That last one is refused here rather than by the cells guard,
57
+ // because the guard throws and this table promises a null.
58
+ // A method is a word or it is nothing at all. The seal writes a payload as
59
+ // JSON, which drops a key whose value it cannot write, so a symbol or a
60
+ // function here would leave as an ask with no method: the empty ask. She
61
+ // would have asked for work, been handed a blueprint, and spent a number on
62
+ // it. Nothing that cannot be sent is sent, and what never left is unreached,
63
+ // which is the one answer that says asking again is safe. This is the same
64
+ // rule args are already held to.
65
+ const named = (method: unknown): boolean => method === undefined || typeof method === 'string';
66
+ const reserved = (id: string): boolean => RESERVED_IDS.includes(id) || CALLS.includes(id) || unkeepable(id);
49
67
  // The name of one relation, as her side files it. A relation is a ward and
50
68
  // a heir, never a heir alone: the heir pk is outer, it rides in the clear on
51
69
  // every lid, and anyone who reads one can quote it back inside an invitation
@@ -224,6 +242,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
224
242
  const calls = {
225
243
  knock: async (inv: Invitation, method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
226
244
  if (gone()) return word('dropped');
245
+ if (!named(method)) return unreached();
227
246
  if (!valid(inv)) return word('invitation'); // S1. nothing is sent
228
247
  // She may knock again, and after take that knock is an ask: the relation
229
248
  // has one home, so it answers on the standing's lane, under the
@@ -283,6 +302,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
283
302
  // the next line. Waiting for the lane is not a reason to lose it.
284
303
  ask: (method?: string, args: JsonObject = {}, wanted?: Wanted): Promise<Answer> => {
285
304
  if (gone()) return Promise.resolve(word('dropped') as Answer);
305
+ if (!named(method)) return Promise.resolve(unreached() as Answer);
286
306
  const keys = at(bind.standings, id);
287
307
  if (!keys) return Promise.resolve(word('dropped') as Answer); // S2. she dropped it between one line and the next
288
308
  const rec = at(cells.standings, id);
@@ -349,6 +369,12 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
349
369
  // it keeps the pk beside the id and nothing else. the invitation IS the key.
350
370
  invite: async (id: string, notes?: JsonObject): Promise<Invitation | null> => {
351
371
  if (gone() || reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null;
372
+ // Notes are values, like everything else the partition keeps, and a
373
+ // spread copies one level: an object inside them would stay the
374
+ // caller's, a handle into her cells that writes past the guard and
375
+ // never says `wrote`, so what a restart brought back would not be
376
+ // what she read. Held to the rule and copied whole, or no invitation.
377
+ if (notes !== undefined && cellFault(notes, 'notes') !== null) return null;
352
378
  // The key first, and nothing written until it exists: a record put
353
379
  // before the mint would be a record a remove in the meantime drops
354
380
  // with no heir to close, and the heir opened after it would name an
@@ -357,7 +383,7 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
357
383
  if (reserved(id) || at(cells.occupants, id) || at(cells.standings, id)) return null; // taken while the key was minted
358
384
  // The notes are the terms the inviter minted under, hers to read on her
359
385
  // gate. She may write more later; nobody outside ever writes them.
360
- put(cells.occupants, id, { id, notes: notes ? { ...notes } : {} });
386
+ put(cells.occupants, id, { id, notes: notes ? (JSON.parse(JSON.stringify(notes)) as JsonObject) : {} });
361
387
  inside.openHeir(k.pk, key, id);
362
388
  put(bind.occupants, id, k.pk);
363
389
  inside.wrote();
@@ -373,5 +399,35 @@ export function buildStance(inside: Inside, key: string, cells: Cells, bind: Bin
373
399
  },
374
400
  },
375
401
  standings,
402
+ // A standing at one of the things this device can do. The same three
403
+ // moves her boot makes, with the harbor's root doing the inviting instead
404
+ // of a being of this ward: ask the ground for the name, knock with what
405
+ // comes back, take it under the id she gave. She is handed the id.
406
+ //
407
+ // The invitation never reaches her, and that is the difference between
408
+ // this relation and every other she holds. Her own invitations are hers
409
+ // to give away, because giving one away is giving away her own relation.
410
+ // This one is the device's, minted for this ward alone, and a value she
411
+ // could copy is a capability she could hand to anyone. A being who wants
412
+ // to lend her device access to another does it in the open, by an ask of
413
+ // her own that forwards to this standing, where her gate reads who is
414
+ // asking and she can stop.
415
+ lend: async (name: string, id: string): Promise<string | null> => {
416
+ if (gone() || typeof name !== 'string' || typeof id !== 'string') return null;
417
+ if (reserved(id) || at(cells.standings, id) || at(cells.occupants, id)) return null;
418
+ // Her half runs inside the harbor's call, so a knock that was refused
419
+ // and a take that lost the id both end with the harbor removing what it
420
+ // minted. Nothing half-lives here either.
421
+ const took = await inside.lend(name, async (inv) => {
422
+ // Read again: the ground was awaited, and a line of hers in between
423
+ // may have taken the id. Nothing is written for a relation she cannot
424
+ // hold, and the invitation goes back unspent.
425
+ if (gone() || reserved(id) || at(cells.standings, id) || at(cells.occupants, id)) return false;
426
+ const out = await calls.knock(inv);
427
+ if (isSilence(out) || isWord(out)) return false;
428
+ return (await calls.take(id, inv)) === id;
429
+ });
430
+ return took ? id : null;
431
+ },
376
432
  };
377
433
  }
package/src/ward/ward.ts CHANGED
@@ -198,7 +198,7 @@ class Self implements BeingLike {
198
198
  #boot(key: string, make: (stance: Stance) => BeingLike | null): Resident | null {
199
199
  const cells = guardCells(at(this.p.beings, key) ?? emptyCells(), () => this.#wrote());
200
200
  const bind = at(this.p.bind, key) ?? emptyBind();
201
- const stance = buildStance(
201
+ const stance: Stance = buildStance(
202
202
  {
203
203
  pk: this.pk,
204
204
  // This stance, not merely this key: unboot and boot again under the
@@ -219,6 +219,11 @@ class Self implements BeingLike {
219
219
  // never came about takes her out again.
220
220
  relate: async (k, id) => (await this.doors.get(k)?.stance.occupants.invite(id)) ?? null,
221
221
  unmake: (k) => void this.#unboot(k),
222
+ // What this device lends this ward's beings. The ward knocks and takes
223
+ // inside the harbor's own call and reads nothing in the value: which
224
+ // names there are, which ward may ask for one, and what becomes of one
225
+ // she did not take, is the harbor's and no word of the ward.
226
+ lend: async (name, take) => (await this.g.lend?.(name, take)) ?? false,
222
227
  wrote: () => this.#wrote(),
223
228
  },
224
229
  key,
@@ -284,10 +289,14 @@ class Self implements BeingLike {
284
289
  return unreached();
285
290
  }
286
291
  const { bytes, ephemeral } = sealed;
287
- // The wait is bounded, and this is the one thing the ward times. A being
288
- // holds three answers and a wait that does not end is none of them: a
289
- // relation that comes back round holds a lane the answer needs, and only a
290
- // bound on the wait can break that. What comes back late is not read.
292
+ // The wait is bounded here, and again around the lane in the stance. Two
293
+ // bounds and not one, because they end two different things: the stance's
294
+ // ends the wait a being is held in, and this one ends the occupancy of the
295
+ // relation's lane. A being holds three answers and a wait that does not
296
+ // end is none of them, and a lane nobody ever leaves is a relation the
297
+ // next ask never reaches. Only a bound on the wire breaks the second, and
298
+ // taking it out would leave one quiet far side holding the lane for good.
299
+ // What comes back late is not read.
291
300
  //
292
301
  // A wait that ran out is `late`, never unreached. Unreached promises
293
302
  // nothing was delivered and is safe to retry; a bound that expired knows
File without changes
File without changes
File without changes