@quo-systems/quo 0.2.10 → 0.2.12

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 (72) hide show
  1. package/SPEC.md +141 -22
  2. package/dist/being/being.js +7 -1
  3. package/dist/being/digest.js +15 -1
  4. package/dist/being/index.d.ts +2 -2
  5. package/dist/being/index.js +2 -2
  6. package/dist/being/silence.d.ts +2 -0
  7. package/dist/being/silence.js +12 -0
  8. package/dist/being/types.d.ts +2 -0
  9. package/dist/being/types.js +25 -0
  10. package/dist/conformance/assert.js +40 -6
  11. package/dist/conformance/beings.d.ts +48 -5
  12. package/dist/conformance/beings.js +39 -8
  13. package/dist/conformance/estate.js +109 -18
  14. package/dist/conformance/index.d.ts +5 -2
  15. package/dist/conformance/index.js +164 -6
  16. package/dist/harbor/core.d.ts +3 -2
  17. package/dist/harbor/core.js +86 -25
  18. package/dist/harbor/dial.js +28 -13
  19. package/dist/harbor/index.d.ts +1 -0
  20. package/dist/harbor/index.js +3 -0
  21. package/dist/harbor/memory.d.ts +3 -3
  22. package/dist/harbor/memory.js +6 -12
  23. package/dist/harbor/reach.js +10 -10
  24. package/dist/ward/allowance.js +7 -2
  25. package/dist/ward/arithmetic.d.ts +1 -0
  26. package/dist/ward/arithmetic.js +11 -3
  27. package/dist/ward/door.d.ts +2 -2
  28. package/dist/ward/door.js +14 -5
  29. package/dist/ward/ground.d.ts +4 -1
  30. package/dist/ward/ground.js +38 -1
  31. package/dist/ward/heirs.d.ts +1 -2
  32. package/dist/ward/heirs.js +13 -14
  33. package/dist/ward/index.d.ts +1 -0
  34. package/dist/ward/index.js +3 -0
  35. package/dist/ward/owner.d.ts +4 -4
  36. package/dist/ward/owner.js +36 -19
  37. package/dist/ward/partition.d.ts +3 -0
  38. package/dist/ward/partition.js +109 -4
  39. package/dist/ward/seal.d.ts +1 -0
  40. package/dist/ward/seal.js +38 -13
  41. package/dist/ward/stance.d.ts +3 -2
  42. package/dist/ward/stance.js +48 -20
  43. package/dist/ward/ward.d.ts +1 -1
  44. package/dist/ward/ward.js +40 -19
  45. package/package.json +1 -1
  46. package/src/being/being.ts +7 -1
  47. package/src/being/digest.ts +15 -2
  48. package/src/being/index.ts +2 -2
  49. package/src/being/silence.ts +14 -0
  50. package/src/being/types.ts +29 -1
  51. package/src/conformance/assert.ts +37 -4
  52. package/src/conformance/beings.ts +41 -10
  53. package/src/conformance/estate.ts +106 -17
  54. package/src/conformance/index.ts +187 -12
  55. package/src/harbor/core.ts +98 -25
  56. package/src/harbor/dial.ts +41 -14
  57. package/src/harbor/index.ts +3 -0
  58. package/src/harbor/memory.ts +8 -14
  59. package/src/harbor/reach.ts +10 -11
  60. package/src/ward/allowance.ts +7 -2
  61. package/src/ward/arithmetic.ts +14 -5
  62. package/src/ward/door.ts +16 -7
  63. package/src/ward/ground.ts +43 -1
  64. package/src/ward/heirs.ts +13 -14
  65. package/src/ward/index.ts +3 -0
  66. package/src/ward/owner.ts +45 -26
  67. package/src/ward/partition.ts +109 -5
  68. package/src/ward/seal.ts +39 -12
  69. package/src/ward/stance.ts +53 -23
  70. package/src/ward/ward.ts +46 -25
  71. package/vectors/framing.json +16 -10
  72. package/vectors/wire.json +4 -4
package/SPEC.md CHANGED
@@ -93,7 +93,13 @@ ground
93
93
 
94
94
  Nothing else is ever passed. A harbor that needs a seventh thing has found a
95
95
  gap in Quo or a leak into the ward. The shape is `Ground` in
96
- `src/ward/ground.ts`.
96
+ `src/ward/ground.ts`, and beside it are the three pieces every harbor in this
97
+ kit builds one out of: `maker`, the code half, which finds a class by own key
98
+ in the first registry that holds it and remembers the object by the cells;
99
+ `entropy`, the one line every terrain has; and `learnPk`, the empty ask, since
100
+ a harbor learns its ward's pk the way anyone learns anything and has no second
101
+ derivation to keep in step. They are convenience and never contract: a kit
102
+ writes its own harbor, and may write these again.
97
103
 
98
104
  Returned:
99
105
 
@@ -104,7 +110,9 @@ Returned:
104
110
  harbor learns from an arrival, never a reason: a pk that only ever brings
105
111
  strangers' bytes is the harbor's to rate or refuse, and what a bound key
106
112
  hears is sealed to its own lid.
107
- - **The ask.** One call: method and args in, object or silence out.
113
+ - **The ask.** One call: method and args in, a value or silence out. A value,
114
+ not an object: what a being answers is held to the value rule and nothing
115
+ narrower, so a string, a number and an array are all answers.
108
116
  In-process and unsealed. This is the one unsealed ask in Quo, and the only
109
117
  way a ward is piloted. Judgment, catching, and the return table are not
110
118
  skipped: a throw inside is silence, and an unreached shape is silence.
@@ -327,7 +335,8 @@ D7 signature fails under an admitted key, or under the key hel
327
335
  refusals to a bound key: a word, nothing written, heard true
328
336
  D8 she is not there `removed`: the occupant record is gone, and this is the key it was
329
337
  bound to when it went. `absent`: the being did not come back this run.
330
- D9 a knock announces nothing `unannounced`: the heir is fresh and next is null. it binds nothing.
338
+ D9 a knock announces nothing `unannounced`: the heir is fresh and next is null, or is the heir
339
+ itself, which is no key of her own. it binds nothing.
331
340
  D10 the number is refused `repeated`: already honoured, or at or below the span.
332
341
  choices: the number spent, the keys rotated, heard true
333
342
  D11 she threw `threw`
@@ -413,7 +422,10 @@ None is an object, and none carries anything but its name. In this kit
413
422
  silence is the symbol `quo.silence` and a word is a frozen object under the
414
423
  symbol key `quo.word` holding its name, both from `src/being/silence.ts`;
415
424
  `isWord` and `wordOf` read them, and `isUnreached` is the one word a being
416
- asks about most.
425
+ asks about most. `told` names a word and leaves everything else exactly as it
426
+ came, so asking which word arrived is one comparison rather than two joined by
427
+ an and: `told(out) === 'late'`. A kit in another language spells it however
428
+ that language spells one value standing for either, and owes nothing here.
417
429
 
418
430
  ### Blueprint, schema, digest
419
431
 
@@ -439,6 +451,16 @@ holding what is not a value is dropped before hashing and an array slot
439
451
  holding one is null, which is what crossing an edge does to them anyway. The
440
452
  reference is `src/being/digest.ts`.
441
453
 
454
+ Two places in RFC 8785 are where languages part, and a kit is held to both by
455
+ `vectors/framing.json`. Keys sort by **UTF-16 code unit**, not by code point
456
+ and not by byte, so an astral character sorts before one in the surrogate
457
+ range: `U+1F600` comes before `U+FB33`, which is the reverse of what a
458
+ language sorting code points gives. Numbers are written as ECMAScript writes
459
+ them, `1e+21` and `1e-7` and `0.3333333333333333`, with a negative zero
460
+ written `0`. A kit that reproduces every ASCII vector and neither of these
461
+ hashes one blueprint to two digests, and every standing between the two wards
462
+ refreshes on every ask.
463
+
442
464
  Capability and state are two axes, never mixed.
443
465
 
444
466
  ```
@@ -498,7 +520,13 @@ cells
498
520
  ```
499
521
 
500
522
  - `digest` is the hash of the blueprint she last fetched by the empty ask.
501
- - `blueprint` is that blueprint.
523
+ It is over what came back, whatever that was.
524
+ - `blueprint` is that blueprint, and only if it is one. A far describe is
525
+ somebody else's code and may answer any value at all; the ward reads it as
526
+ a blueprint before writing it as one, a list of asks with a name and an
527
+ input each, and writes `null` when it is not. A side walks `asks` by name,
528
+ and a side that broke on a far ward's answer would be one kit made wrong
529
+ by another. The answer itself still goes to whoever asked, unread.
502
530
  - `seen` is the digest her ward last saw arrive with an answer. A silent
503
531
  refresh leaves it untouched.
504
532
  - `notes` is hers. Quo never reads it. Tier, expiry, kinship between an
@@ -585,8 +613,22 @@ An invitation to her is `{ ward }` alone. One with a secret and no heir is
585
613
  not an invitation. She may be taken as a standing on that invitation, and
586
614
  such a standing has no heir and never rotates.
587
615
 
616
+ The signature is checked here for no property, and the check is kept anyway.
617
+ Whoever asks chose that key a moment ago, the box is already authenticated to
618
+ the ward's padlock, and the door binds nothing, so nothing is proven by it and
619
+ nothing would be lost by dropping it. It stays because the payload has one
620
+ shape and the door has one path: `by`, `next` and `seq` are required of her
621
+ ask as of every other, and honoured by nobody. A kit author who goes looking
622
+ for the property behind this signature will not find one, and is not missing
623
+ anything.
624
+
588
625
  She is reached without a heir, so the door keeps nothing for whoever asked:
589
- no key it vouched for, and no count. The signature is still checked, under
626
+ no key it vouched for, and no count. The bit beside her reply is `heard`
627
+ false, as it is for every stranger: no key this door holds spoke. She is the
628
+ one place a stranger is answered by design, so she is the one place the
629
+ harbor's rating must still see a stranger arrive. A harbor that saves before
630
+ its reply goes out follows the ward writing, never the bit, since a public
631
+ being writes in her cells like anyone. The signature is still checked, under
590
632
  whatever key signed. Once-only delivery does not reach her: the same sealed
591
633
  bytes presented twice are delivered twice, and the number the payload carries
592
634
  is required and honoured by nobody. A count per voice would be memory a
@@ -638,7 +680,17 @@ key does both: an Ed25519 pair to sign replies, an X25519 padlock every ask
638
680
  is sealed to. Beings never own a padlock.
639
681
 
640
682
  - **The ward key** comes from the seed. Its pk on the wire is the signing pk
641
- then the padlock, 128 hex, and it routes.
683
+ then the padlock, 128 hex, and it routes. Two curves, and each secret is
684
+ HKDF-SHA-256 of the seed under its own label, `quo-ward-sign` and
685
+ `quo-ward-seal`, empty salt, 32 bytes out. Fed the seed straight the two
686
+ scalars would still differ, because Ed25519 hashes what it is given and
687
+ X25519 clamps it raw, but that is an accident of the two designs and no
688
+ separation: one secret would be doing two jobs with nothing said about it,
689
+ and a second kit would have to reproduce a construction nobody named. A
690
+ seed handed in as bytes of the key length is the seed; anything else, text
691
+ or bytes of another length, is SHA-256 first, so a thirty-two character
692
+ name is a name and not a key for being the right size.
693
+ `vectors/framing.json` pins it.
642
694
  - **The heir** is the key the inviting ward mints at invite, for one id. It
643
695
  keeps the heir's pk beside the id and gives the secret away. The
644
696
  invitation IS the heir: ward pk, heir pk, heir secret. Rotation one: the
@@ -810,7 +862,11 @@ share no key learn nothing from each other.
810
862
 
811
863
  Four algorithms, named once and never negotiated: Ed25519 signs, X25519
812
864
  agrees, SHA-256 hashes, AES-256-GCM encrypts with key and nonce derived
813
- together by HKDF-SHA-256 under an empty salt and the label `quo-seal`. The
865
+ together by HKDF-SHA-256 under an empty salt and the label `quo-seal`. That
866
+ label is the message cipher's and no other: HKDF appears twice in this kit,
867
+ here from an agreement and again at the ward key from a seed, and the three
868
+ labels, `quo-seal`, `quo-ward-sign`, `quo-ward-seal`, are three so that two
869
+ derivations never answer to one name. The
814
870
  ephemeral pk is the additional authenticated data of every box. A small-order
815
871
  public key verifies nothing; an all-zero agreement is refused. All four are
816
872
  in WebCrypto, which is why the ward names no package -- on any terrain that
@@ -943,8 +999,10 @@ partition
943
999
  occupants id -> heir pk
944
1000
  knocks <ward>:<heir> | public:<ward> -> { current, next, spoke, sent, seq } before take
945
1001
  spoke: bytes came back once. sent: bytes went out once, answered or not
1002
+ bounded, oldest out, and one that answered outlives one that never did
946
1003
  answered <ward>:<heir> | public:<ward> -> true knocked and answered
947
- minted the last eight pks her side minted
1004
+ minted the last eight pks her side minted. nothing in the ward reads it: it is there to
1005
+ be looked at, and a kit that keeps the list empty holds the same ward
948
1006
  heirs heir pk -> { being, id, current, announced, fresh, mark, spent } the door's view of every occupant
949
1007
  gone heir pk -> { current, announced } the keys held when an id was removed, bounded, so their
950
1008
  holder hears `removed` and nobody else hears a thing
@@ -956,6 +1014,12 @@ alone: an invitation carries the heir to whoever it is for, and anyone
956
1014
  holding one could quote that heir back inside an invitation naming a ward of
957
1015
  their own.
958
1016
 
1017
+ Only take ends a knock record, so the list is bounded like `gone`: past the
1018
+ count the oldest go, and one that was answered outlives one that never was,
1019
+ since an answered knock is a relation she may still take. A being who knocks
1020
+ without ever taking keeps a fixed number of keys behind her, not one per
1021
+ invitation she ever met.
1022
+
959
1023
  The version is one value and it does not move until 1.0.0. There is nobody
960
1024
  holding a partition of another shape to tell apart: if the shape changes, a
961
1025
  partition is thrown away, not migrated and not counted. A ward opening a
@@ -963,6 +1027,17 @@ version it cannot read throws at birth and does not boot. Birth is where a
963
1027
  ward is allowed to be loud; silence is the door's word, for asks that were
964
1028
  made, and no ask has been made yet.
965
1029
 
1030
+ The version says which shape, and the ward reads the shape as well, once, at
1031
+ birth: every field above that the ward, the door or the heirs later act on
1032
+ without looking again. A partition this ward wrote is that shape by
1033
+ construction. One adopted from elsewhere was written by a hand, another kit,
1034
+ or a file that was cut short, and a `spent` that is not a list or a `mark`
1035
+ that is not a number is a door that honours every number, or one that
1036
+ rejects where it promised a word. A ward that meets one throws at birth with
1037
+ the path that failed, which names what is wrong to whoever is holding the
1038
+ file. A being's own cells are read no further than the two records the ward
1039
+ keeps in them: what she puts in her own is hers, of any shape JSON carries.
1040
+
966
1041
  What a ward can read is a list, `READS`, and today it has one member. The
967
1042
  list and the step that carries an older partition forward exist before
968
1043
  1.0.0 because after it there is nowhere to put them. A partition holds every
@@ -1054,7 +1129,11 @@ anything else -> { error: 'unknown ask' }
1054
1129
  every relation she had, reachable by nobody at the bare pk and told by
1055
1130
  nobody she was replaced. Marking the one already public again is
1056
1131
  answered, and changes nothing. A public being absent this run is
1057
- reachable by nobody already, so the mark may move to another.
1132
+ reachable by nobody already, so the mark may move to another. A null key
1133
+ takes the mark off: the being stays booted with every relation she holds,
1134
+ and the ward answers arrivals for no heir with silence as one that never
1135
+ had a public being does. Retiring her is not destroying her, and the mark
1136
+ is free for another.
1058
1137
  - Invite on the ward's own pk mints an owner, and only the root may ask it:
1059
1138
  from the door it is answered as an invite on nobody. The ward has
1060
1139
  occupants, its owners, and no standings: a knock for the ward itself is a
@@ -1142,8 +1221,15 @@ Not stressed with:
1142
1221
 
1143
1222
  A harbor is what a device already has, offered to Quo: processes, storage, a
1144
1223
  network, entropy, a clock. It fits into a device carved up by its owner,
1145
- users, containers, mounts, profiles, and asks for none of them. It knows no
1146
- being, holds no id, reads no partition, and is nobody outside its device.
1224
+ users, containers, mounts, profiles, and asks for none of them. It judges
1225
+ nothing, holds no id, chooses no class, and is nobody outside its device.
1226
+
1227
+ It makes every being of every ward it serves, because `instantiate` is in
1228
+ the ground and the ward calls it, and it keeps what it made. That is not
1229
+ knowing a being. It has her object and knows nothing about her: not what she
1230
+ answers, not who her occupants are, not which of her cells is a secret. It
1231
+ never reads a partition to decide anything, and it never hands one out
1232
+ except as the values a store keeps.
1147
1233
 
1148
1234
  The harbor:
1149
1235
 
@@ -1163,6 +1249,16 @@ The harbor:
1163
1249
  - Passes every ward the ground, once, at birth. Receives a door and an ask.
1164
1250
  Hands the ask to exactly one holder, by the device's own rules. That
1165
1251
  holder is the ward's owner.
1252
+ - Hands its own device's code the beings it made for a ward, by key, and the
1253
+ keys it has. This is not a path around a door. The object is already in
1254
+ that process, made there a moment ago by the harbor itself, and reaching
1255
+ it is the device's own code calling its own object, the same reach a being
1256
+ has on one she booted herself. Nothing of it crosses an edge, so there is
1257
+ no door it could have passed instead. A key with no object is a being who
1258
+ is not here this run. What the device does not get is the partition: it
1259
+ holds every seed the ward has, and a side that wants to know which beings
1260
+ there are, or which one is public, asks the ward through the ask pointer,
1261
+ which is the only thing that answers for a ward anyway.
1166
1262
  - Keeps the map of ward pk to door for its own wards, and ward pk to reach
1167
1263
  for foreign ones. Learns its own wards' pks by asking them. Learns foreign
1168
1264
  pks however it likes.
@@ -1189,8 +1285,8 @@ Not stressed with:
1189
1285
  silence, and it never answers on a ward's behalf.
1190
1286
  - Fanning out, broadcasting, or forwarding to a pk other than the one it was
1191
1287
  given. One pk is one ward is one door.
1192
- - Beings, ids, keys inside a relation, classes as things to choose,
1193
- invitations, or what any partition contains.
1288
+ - Choosing a class, deciding an id, reading the keys inside a relation,
1289
+ minting an invitation, or acting on anything a partition contains.
1194
1290
  - Judging anything. A ward judges its door. The harbor delivers to it.
1195
1291
  - Speaking Quo. It is nobody's occupant and holds no standing anywhere. The
1196
1292
  harbor is not a ward and is nobody's.
@@ -1207,7 +1303,9 @@ the harbor core then passes the same suite over memory stores.
1207
1303
 
1208
1304
  What a harbor on a device keeps is a ward's three parts under one name: the
1209
1305
  seed, the partition, and a **ward record**, which says where the class
1210
- bodies come from and which being is the user's. That is the **store**,
1306
+ bodies come from and which being is the user's. `user` is a key and nothing
1307
+ else: a label the device's own code hands back to get the being it made,
1308
+ never read to route a byte or judge an ask. That is the **store**,
1211
1309
  `src/harbor/store.ts`: list, load, put, save the partition when the ward wrote,
1212
1310
  save the record after a boot learned the pk, take a ward out as the first
1213
1311
  move of a migration, and keep the directory's hints, a pk to a URL, across
@@ -1311,18 +1409,28 @@ with a wait that doubles from a second to thirty when the line drops, and
1311
1409
  while it is dialed it is one of the harbor's fallbacks, where a pk nobody
1312
1410
  here knows is sent. A device that slept, a phone in a pocket, comes back
1313
1411
  with that wait frozen and, often, its socket closed under it by the system
1314
- without a word, so the dialer takes one call from the device, `wake`: a
1315
- line in hand is made to announce, and a dead one closes under the word and
1316
- is dialed again; a line that is down is dialed now, the wait called off; a
1317
- dial in flight is left alone. Fallbacks are a list and a pk is tried down it in
1412
+ without a word, so the dialer takes one call from the device, `wake`:
1413
+ whatever line it holds, open or still opening, is put down and a new one is
1414
+ dialed now, the frozen wait called off. Nothing can tell a line the system
1415
+ dropped from a live one, and asking would cost a round trip the four words
1416
+ do not have; an announce written into a dead line is taken into the send
1417
+ buffer and errors only when TCP gives up, minutes later, and for all of it
1418
+ every ask to a pk bound behind that line hears `late`. Dialing costs one
1419
+ handshake when the line was fine, and an ask made meanwhile waits on the new
1420
+ socket. A line put down this way closes when the system gets round to it,
1421
+ gives back what it took, and dials nothing: only the line the dialer still
1422
+ holds dials again. Fallbacks are a list and a pk is tried down it in
1318
1423
  order: a harbor may dial more than one listener, and a rendezvous is a
1319
1424
  listener and nothing more, so a ward is never reachable through one place
1320
1425
  by anything but its own choice. A line that drops takes only its own place
1321
1426
  out of the list. A ward is born on the core by minting a seed under a name,
1322
1427
  leaves it by being dropped, the partition written first, and arrives by
1323
- being adopted, same seed, same pk. What a terrain adds is a store of its
1324
- own, a loader for the class bodies, and the lease its device offers; what
1325
- it never touches is a ward.
1428
+ being adopted, same seed, same pk. Adopting is the one path a partition
1429
+ written elsewhere takes, so it is where the ward's shape check is met: one
1430
+ that will not boot is not adopted, it goes back out of the store, and the
1431
+ only copy is the one the caller is still holding. What a terrain adds is a
1432
+ store of its own, a loader for the class bodies, and the lease its device
1433
+ offers; what it never touches is a ward.
1326
1434
 
1327
1435
  ## The being
1328
1436
 
@@ -1391,6 +1499,17 @@ The raw shape needs no import from the kit. The kit also offers a base class,
1391
1499
  `for(occupant, asker)` decides whether this asker sees the ask, and so
1392
1500
  whether this asker may call it: what she shows is what she can be asked,
1393
1501
  one gate for describe and for dispatch.
1502
+ - Both statics are read off the class the object was made from, and a
1503
+ subclass that declares either **replaces** its parent's rather than adding
1504
+ to it. That is the rule and not an oversight: a being's blueprint is
1505
+ exactly what the class in front of you declares, in the order she chose,
1506
+ and a merge would hand her asks she may mean to drop and an order she did
1507
+ not write. A subclass that means to extend says so, in one spelling,
1508
+ `static override asks = { ...Parent.asks, mine: {} }`, and the same for
1509
+ cells. A parent that means to be subclassed at all annotates rather than
1510
+ infers, `static override cells: JsonObject = { ... }`, since this language
1511
+ holds a subclass's static side to its parent's and would otherwise refuse a
1512
+ subclass declaring fewer keys than the parent happened to write.
1394
1513
  - `answer` is written for her. The empty ask is `describe(asker)`, which she
1395
1514
  may override by hand. A named ask calls the method of that name with
1396
1515
  `(args, asker)`. Anything not declared, hidden from this asker, or
@@ -12,6 +12,12 @@ const method = (self, name) => {
12
12
  }
13
13
  return false;
14
14
  };
15
+ // Both statics below are read off the class the object was made from, so a
16
+ // subclass declaring either replaces its parent's rather than adding to it.
17
+ // That is the rule: her blueprint is exactly what the class in front of you
18
+ // declares, in the order she chose, and merging down a chain would hand her
19
+ // asks she may mean to drop and an order she did not write. A subclass that
20
+ // means to extend says so, `static override asks = { ...Parent.asks, mine: {} }`.
15
21
  export class Being {
16
22
  // Her cells' defaults. Merged in at birth, only where a key is missing, so
17
23
  // a restart keeps what she wrote.
@@ -89,7 +95,7 @@ export class Being {
89
95
  // would answer with her stance, `toString` with a string, and neither is
90
96
  // an ask she wrote. Only her own keys are asks, which is what describe
91
97
  // shows. What she shows is what she can be asked.
92
- const spec = typeof method === 'string' && Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
98
+ const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
93
99
  if (!spec || (spec.for && !spec.for(this.occupant(asker), asker)))
94
100
  return { error: 'unknown ask' };
95
101
  const fn = this[method];
@@ -19,5 +19,19 @@ export const canonical = (v) => {
19
19
  }
20
20
  return JSON.stringify(v);
21
21
  };
22
+ // Hex and the guard are spelled here and again in `src/ward/arithmetic.ts`,
23
+ // which is the price of the boundary: nothing under `src/being` imports
24
+ // anything above it, because this is the whole world a being's own code sees
25
+ // and a being reaching the ward is the thing the shape is against.
22
26
  const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
23
- export const digest = async (blueprint) => hex(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
27
+ // `crypto.subtle` is read at the call and never captured at load. A browser
28
+ // on a plain http:// origin has `crypto` without `subtle`, and a terrain may
29
+ // install one after this module is first imported; either way the failure is
30
+ // one sentence and not a TypeError from inside a digest nobody can read.
31
+ const subtle = () => {
32
+ const s = globalThis.crypto?.subtle;
33
+ if (!s)
34
+ throw new Error('this terrain has no crypto.subtle: a being needs a secure context to be described');
35
+ return s;
36
+ };
37
+ export const digest = async (blueprint) => hex(await subtle().digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
@@ -1,5 +1,5 @@
1
1
  export { Being, type AskSpec } from './being.ts';
2
- export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.ts';
2
+ export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
3
3
  export { digest, canonical } from './digest.ts';
4
- export { OWNER, PUBLIC, RESERVED_IDS } from './types.ts';
4
+ export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs } from './types.ts';
5
5
  export type * from './types.ts';
@@ -1,6 +1,6 @@
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 } from './being.js';
4
- export { silence, isSilence, unreached, isUnreached, word, isWord, wordOf, DOOR_WORDS, isDoorWord } from './silence.js';
4
+ export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.js';
5
5
  export { digest, canonical } from './digest.js';
6
- export { OWNER, PUBLIC, RESERVED_IDS } from './types.js';
6
+ export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs } from './types.js';
@@ -6,5 +6,7 @@ export declare const isDoorWord: (s: unknown) => s is DoorWord;
6
6
  export declare const word: <W extends WordName>(name: W) => Word<W>;
7
7
  export declare const isWord: (x: unknown) => x is Word;
8
8
  export declare const wordOf: (x: Word) => WordName;
9
+ export declare const answered: <T>(x: T) => x is Exclude<T, typeof silence | Word>;
10
+ export declare const told: (x: unknown) => unknown;
9
11
  export declare const unreached: () => Unreached;
10
12
  export declare const isUnreached: (x: unknown) => x is Unreached;
@@ -22,6 +22,18 @@ export const word = (name) => {
22
22
  };
23
23
  export const isWord = (x) => x !== null && typeof x === 'object' && typeof x[WORD_KEY] === 'string';
24
24
  export const wordOf = (x) => x[WORD_KEY];
25
+ // What came back is an answer and not one of the two things that are not
26
+ // answers. A being who only wants the object writes one test instead of
27
+ // three, and the three shapes stay three shapes: silence is no answer, a word
28
+ // is her ward telling her why there is none.
29
+ export const answered = (x) => !isSilence(x) && !isWord(x);
30
+ // What she was told, with a word given its name and everything else left
31
+ // exactly as it came. One value to compare against, so asking which word came
32
+ // back is one test and not two joined by an and: `told(out) === 'late'` says
33
+ // what `isWord(out) && wordOf(out) === 'late'` says, and says which answer
34
+ // arrived instead when it is wrong, where the pair collapses to false and
35
+ // names nothing. Silence stays the symbol it is, and an object stays itself.
36
+ export const told = (x) => (isWord(x) ? wordOf(x) : x);
25
37
  // Unreached: no far door was reached. Nothing is known to have been
26
38
  // delivered, so asking again is safe. A being cannot produce it: a ward that
27
39
  // sees a word come out of a being reads it as her having thrown.
@@ -18,6 +18,7 @@ export type Invitation = {
18
18
  heir?: string;
19
19
  secret?: string;
20
20
  };
21
+ export declare const invitationArgs: (inv: Invitation) => JsonObject;
21
22
  export type Schema = JsonObject;
22
23
  export type Ask = {
23
24
  name: string;
@@ -29,6 +30,7 @@ export type Blueprint = {
29
30
  asks: Ask[];
30
31
  notes: Json;
31
32
  };
33
+ export declare const isBlueprint: (v: unknown) => v is Blueprint;
32
34
  export type StandingRecord = {
33
35
  id: string;
34
36
  digest: string | null;
@@ -13,4 +13,29 @@
13
13
  export const OWNER = 'OWNER';
14
14
  export const PUBLIC = 'PUBLIC';
15
15
  export const RESERVED_IDS = [OWNER, PUBLIC];
16
+ // An invitation as one object of values, which is what it already is. A being
17
+ // who hands one on sends it as args, and args are values; this is the one
18
+ // place that is spelled, so nobody spells it with a cast. Absent stays absent:
19
+ // an invitation to a public being carries no heir and no secret, and a key
20
+ // present and undefined is not the same object once JSON has been through it.
21
+ export const invitationArgs = (inv) => ({
22
+ ward: inv.ward,
23
+ ...(inv.heir !== undefined ? { heir: inv.heir } : {}),
24
+ ...(inv.secret !== undefined ? { secret: inv.secret } : {}),
25
+ });
26
+ // Whether what came back from an empty ask is a blueprint. Every describe on
27
+ // the far side of a door is somebody else's code, so nothing may be written
28
+ // into a standing's record as a blueprint without being read as one first: a
29
+ // side walks `asks` by name, and a list of anything else, or no list at all,
30
+ // is a side that breaks on a far ward's answer. The asks are checked to the
31
+ // depth a side reads them, a name and an input each, and no further: what a
32
+ // far being puts beside those is hers.
33
+ export const isBlueprint = (v) => {
34
+ if (v === null || typeof v !== 'object' || Array.isArray(v))
35
+ return false;
36
+ const { asks, notes } = v;
37
+ if (!Array.isArray(asks) || notes === undefined)
38
+ return false;
39
+ return asks.every((a) => a !== null && typeof a === 'object' && !Array.isArray(a) && typeof a.name === 'string' && a.input !== null && typeof a.input === 'object' && !Array.isArray(a.input));
40
+ };
16
41
  export const WORD_KEY = Symbol.for('quo.word');
@@ -15,6 +15,15 @@ const show = (v) => {
15
15
  return 'undefined';
16
16
  if (typeof v === 'function')
17
17
  return `[function ${v.name}]`;
18
+ // A ward's word is a frozen object under one symbol key, and JSON shows it
19
+ // as `{}`. A red estate chapter saying `{} is not {}` names nothing, so a
20
+ // value whose own keys are all symbols is shown by its keys.
21
+ if (v !== null && typeof v === 'object') {
22
+ const keys = Object.keys(v);
23
+ const syms = Object.getOwnPropertySymbols(v).filter((s) => Object.getOwnPropertyDescriptor(v, s).enumerable);
24
+ if (keys.length === 0 && syms.length > 0)
25
+ return `{ ${syms.map((s) => `${s.toString()}: ${show(v[s])}`).join(', ')} }`;
26
+ }
18
27
  try {
19
28
  return JSON.stringify(v) ?? Object.prototype.toString.call(v);
20
29
  }
@@ -28,22 +37,47 @@ const fail = (why, message) => {
28
37
  // Structural, and strict about shape the way `node:assert/strict` is: a value
29
38
  // is not equal to one of another kind, and an array is not equal to an object
30
39
  // that happens to hold the same keys.
40
+ // The four kinds this compares, and every other object is refused rather than
41
+ // guessed at. A Date, a RegExp, a boxed primitive and a class instance each
42
+ // carry state no key walk can see, so two of them with the same keys are
43
+ // equal here and unequal in node, which is a wrong yes: the one answer a
44
+ // suite must never give. A refusal is a red test that says so.
45
+ const KNOWN = [Object.prototype, null, Array.prototype, Uint8Array.prototype];
46
+ // What node compares: own enumerable keys, strings then enumerable symbols.
47
+ // Not `Reflect.ownKeys`, which also lists what a definition hid.
48
+ const keysOf = (v) => [...Object.keys(v), ...Object.getOwnPropertySymbols(v).filter((s) => Object.getOwnPropertyDescriptor(v, s).enumerable)];
31
49
  export function same(a, b) {
32
50
  if (Object.is(a, b))
33
51
  return true;
34
52
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
35
53
  return false;
36
- if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
54
+ const proto = Object.getPrototypeOf(a);
55
+ if (proto !== Object.getPrototypeOf(b))
37
56
  return false;
57
+ if (!KNOWN.includes(proto))
58
+ return false; // a Date, a RegExp, a boxed number, a class of hers
38
59
  if (Array.isArray(a) !== Array.isArray(b))
39
60
  return false;
40
- if (Array.isArray(a) && Array.isArray(b))
41
- return a.length === b.length && a.every((x, i) => same(x, b[i]));
42
61
  if (a instanceof Uint8Array && b instanceof Uint8Array)
43
62
  return a.length === b.length && a.every((x, i) => x === b[i]);
44
- if (a instanceof Map || b instanceof Map || a instanceof Set || b instanceof Set)
45
- return false; // the suite has none, and a wrong yes is worse than a refusal
46
- const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
63
+ // An array is its indices and whatever else was hung on it, and a hole is
64
+ // not a present undefined: `[, 1]` and `[undefined, 1]` differ in node and
65
+ // must differ here. `every` skips holes, so the loop is written out.
66
+ if (Array.isArray(a) && Array.isArray(b)) {
67
+ if (a.length !== b.length)
68
+ return false;
69
+ for (let i = 0; i < a.length; i += 1) {
70
+ if (i in a !== i in b)
71
+ return false;
72
+ if (i in a && !same(a[i], b[i]))
73
+ return false;
74
+ }
75
+ const extra = (v) => keysOf(v).filter((k) => typeof k !== 'string' || !/^(?:0|[1-9]\d*)$/.test(k));
76
+ const xa = extra(a), xb = extra(b);
77
+ const at = (v, k) => v[k];
78
+ return xa.length === xb.length && xa.every((k) => xb.includes(k) && same(at(a, k), at(b, k)));
79
+ }
80
+ const ka = keysOf(a), kb = keysOf(b);
47
81
  if (ka.length !== kb.length)
48
82
  return false;
49
83
  return ka.every((k) => kb.includes(k) && same(a[k], b[k]));
@@ -1,5 +1,5 @@
1
1
  import { Being } from '../being/being.ts';
2
- import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
2
+ import type { Asker, Blueprint, Invitation, Json, JsonObject, OccupantRecord, Reply, Stance } from '../being/types.ts';
3
3
  export declare class Printer extends Being {
4
4
  static cells: {
5
5
  jobs: JsonObject[];
@@ -14,8 +14,8 @@ export declare class Printer extends Being {
14
14
  };
15
15
  describe(): Blueprint;
16
16
  print({ doc }: JsonObject, asker: Asker): {
17
- printed: string | number | boolean | import("../being/types.ts").Json[] | {
18
- [key: string]: import("../being/types.ts").Json;
17
+ printed: string | number | boolean | Json[] | {
18
+ [key: string]: Json;
19
19
  } | null;
20
20
  };
21
21
  }
@@ -68,7 +68,7 @@ export declare class Shop extends Being {
68
68
  error?: undefined;
69
69
  retry?: undefined;
70
70
  ok: boolean;
71
- receipt: import("../being/types.ts").Json;
71
+ receipt: Json;
72
72
  }>;
73
73
  refund(): {
74
74
  ok: boolean;
@@ -85,7 +85,9 @@ export declare class Customer extends Being {
85
85
  };
86
86
  };
87
87
  };
88
- join(invitation: Invitation): Promise<typeof import("../being/silence.ts").silence | import("../being/types.ts").Word | import("../being/types.ts").Json>;
88
+ join(invitation: Invitation): Promise<string | number | boolean | typeof import("../being/silence.ts").silence | Json[] | import("../being/types.ts").Word<import("../being/types.ts").WordName> | {
89
+ [key: string]: Json;
90
+ } | null>;
89
91
  buy(item: string): Promise<import("../being/types.ts").Answer>;
90
92
  learn(): Promise<import("../being/types.ts").Answer>;
91
93
  hi(_args: JsonObject, asker: Asker): {
@@ -97,6 +99,47 @@ export declare class Echo {
97
99
  constructor(stance: Stance);
98
100
  answer(asker: Asker, method?: string, args?: JsonObject): Reply;
99
101
  }
102
+ export declare class Maker extends Being {
103
+ static cells: {
104
+ made: string[];
105
+ };
106
+ static asks: {
107
+ open: {
108
+ input: {
109
+ type: string;
110
+ properties: {
111
+ class: {
112
+ type: string;
113
+ };
114
+ key: {
115
+ type: string;
116
+ };
117
+ id: {
118
+ type: string;
119
+ };
120
+ };
121
+ required: string[];
122
+ };
123
+ };
124
+ say: {
125
+ input: {
126
+ type: string;
127
+ properties: {
128
+ id: {
129
+ type: string;
130
+ };
131
+ };
132
+ required: string[];
133
+ };
134
+ };
135
+ };
136
+ open({ class: className, key, id }: JsonObject): Promise<{
137
+ made: string | null;
138
+ }>;
139
+ say({ id }: JsonObject): Promise<{
140
+ said: Json;
141
+ }>;
142
+ }
100
143
  export declare class Member extends Being {
101
144
  static cells: {
102
145
  heard: JsonObject[];