nervur 0.19.2 → 0.20.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 (205) hide show
  1. package/README.md +66 -101
  2. package/dist/being/being.d.ts +7 -15
  3. package/dist/being/being.js +16 -67
  4. package/dist/being/digest.d.ts +1 -1
  5. package/dist/being/digest.js +7 -27
  6. package/dist/being/faculty.d.ts +9 -0
  7. package/dist/being/faculty.js +68 -0
  8. package/dist/being/index.d.ts +4 -5
  9. package/dist/being/index.js +5 -5
  10. package/dist/being/types.d.ts +35 -62
  11. package/dist/being/types.js +3 -57
  12. package/dist/being/words.d.ts +13 -0
  13. package/dist/being/words.js +14 -0
  14. package/dist/contract/index.d.ts +26 -0
  15. package/dist/contract/index.js +19 -0
  16. package/dist/crypto/aes.d.ts +3 -0
  17. package/dist/crypto/aes.js +24 -0
  18. package/dist/crypto/bytes.d.ts +6 -0
  19. package/dist/crypto/bytes.js +33 -0
  20. package/dist/crypto/ed25519.d.ts +3 -0
  21. package/dist/crypto/ed25519.js +85 -0
  22. package/dist/crypto/hash.d.ts +2 -0
  23. package/dist/crypto/hash.js +11 -0
  24. package/dist/crypto/index.d.ts +7 -0
  25. package/dist/crypto/index.js +10 -0
  26. package/dist/crypto/json.d.ts +8 -0
  27. package/dist/crypto/json.js +250 -0
  28. package/dist/crypto/mlkem.d.ts +12 -0
  29. package/dist/crypto/mlkem.js +36 -0
  30. package/dist/crypto/subtle.d.ts +10 -0
  31. package/dist/crypto/subtle.js +29 -0
  32. package/dist/crypto/x25519.d.ts +2 -0
  33. package/dist/crypto/x25519.js +21 -0
  34. package/dist/folder/index.d.ts +13 -0
  35. package/dist/folder/index.js +125 -0
  36. package/dist/harbor/dock.d.ts +19 -0
  37. package/dist/harbor/dock.js +92 -0
  38. package/dist/harbor/harbor.d.ts +22 -0
  39. package/dist/harbor/harbor.js +84 -0
  40. package/dist/harbor/index.d.ts +4 -10
  41. package/dist/harbor/index.js +5 -18
  42. package/dist/harbor/registry.d.ts +7 -0
  43. package/dist/harbor/registry.js +13 -0
  44. package/dist/harbor/terrain.d.ts +8 -0
  45. package/dist/harbor/terrain.js +2 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.js +14 -0
  48. package/dist/pointer/bodies.d.ts +20 -0
  49. package/dist/pointer/bodies.js +45 -0
  50. package/dist/pointer/index.d.ts +2 -0
  51. package/dist/pointer/index.js +4 -0
  52. package/dist/pointer/world.d.ts +34 -0
  53. package/dist/pointer/world.js +76 -0
  54. package/dist/quo/door.d.ts +34 -0
  55. package/dist/quo/door.js +172 -0
  56. package/dist/quo/index.d.ts +8 -0
  57. package/dist/quo/index.js +11 -0
  58. package/dist/quo/invitation.d.ts +7 -0
  59. package/dist/quo/invitation.js +15 -0
  60. package/dist/quo/keys.d.ts +40 -0
  61. package/dist/quo/keys.js +79 -0
  62. package/dist/quo/payload.d.ts +15 -0
  63. package/dist/quo/payload.js +55 -0
  64. package/dist/quo/relations.d.ts +40 -0
  65. package/dist/quo/relations.js +33 -0
  66. package/dist/quo/reply.d.ts +13 -0
  67. package/dist/quo/reply.js +35 -0
  68. package/dist/quo/seal.d.ts +42 -0
  69. package/dist/quo/seal.js +78 -0
  70. package/dist/quo/standing.d.ts +39 -0
  71. package/dist/quo/standing.js +90 -0
  72. package/dist/ward/allowance.d.ts +13 -9
  73. package/dist/ward/allowance.js +28 -67
  74. package/dist/ward/cells.d.ts +6 -6
  75. package/dist/ward/cells.js +67 -179
  76. package/dist/ward/index.d.ts +5 -10
  77. package/dist/ward/index.js +6 -13
  78. package/dist/ward/partition.d.ts +44 -60
  79. package/dist/ward/partition.js +56 -285
  80. package/dist/ward/stance.d.ts +29 -20
  81. package/dist/ward/stance.js +173 -406
  82. package/dist/ward/ward-being.d.ts +33 -0
  83. package/dist/ward/ward-being.js +76 -0
  84. package/dist/ward/ward.d.ts +43 -11
  85. package/dist/ward/ward.js +215 -363
  86. package/package.json +19 -33
  87. package/src/being/being.ts +31 -78
  88. package/src/being/digest.ts +7 -35
  89. package/src/being/faculty.ts +66 -0
  90. package/src/being/index.ts +5 -6
  91. package/src/being/types.ts +47 -145
  92. package/src/being/words.ts +31 -0
  93. package/src/contract/index.ts +44 -0
  94. package/src/crypto/aes.ts +26 -0
  95. package/src/crypto/bytes.ts +37 -0
  96. package/src/crypto/ed25519.ts +84 -0
  97. package/src/crypto/hash.ts +14 -0
  98. package/src/crypto/index.ts +10 -0
  99. package/src/crypto/json.ts +241 -0
  100. package/src/crypto/mlkem.ts +38 -0
  101. package/src/crypto/subtle.ts +33 -0
  102. package/src/crypto/x25519.ts +21 -0
  103. package/src/folder/index.ts +134 -0
  104. package/src/harbor/dock.ts +101 -0
  105. package/src/harbor/harbor.ts +105 -0
  106. package/src/harbor/index.ts +5 -19
  107. package/src/harbor/registry.ts +20 -0
  108. package/src/harbor/terrain.ts +13 -0
  109. package/src/index.ts +20 -0
  110. package/src/pointer/bodies.ts +47 -0
  111. package/src/pointer/index.ts +4 -0
  112. package/src/pointer/world.ts +91 -0
  113. package/src/quo/door.ts +178 -0
  114. package/src/quo/index.ts +11 -0
  115. package/src/quo/invitation.ts +15 -0
  116. package/src/quo/keys.ts +91 -0
  117. package/src/quo/payload.ts +61 -0
  118. package/src/quo/relations.ts +62 -0
  119. package/src/quo/reply.ts +38 -0
  120. package/src/quo/seal.ts +101 -0
  121. package/src/quo/standing.ts +111 -0
  122. package/src/stand/main.ts +19 -0
  123. package/src/stand/stand.ts +178 -0
  124. package/src/ward/allowance.ts +37 -75
  125. package/src/ward/cells.ts +63 -176
  126. package/src/ward/index.ts +6 -17
  127. package/src/ward/partition.ts +86 -326
  128. package/src/ward/stance.ts +185 -420
  129. package/src/ward/ward-being.ts +97 -0
  130. package/src/ward/ward.ts +229 -363
  131. package/dist/being/lent.d.ts +0 -32
  132. package/dist/being/lent.js +0 -72
  133. package/dist/being/silence.d.ts +0 -12
  134. package/dist/being/silence.js +0 -41
  135. package/dist/conformance/assert.d.ts +0 -11
  136. package/dist/conformance/assert.js +0 -106
  137. package/dist/conformance/beings.d.ts +0 -199
  138. package/dist/conformance/beings.js +0 -188
  139. package/dist/conformance/estate.d.ts +0 -5
  140. package/dist/conformance/estate.js +0 -388
  141. package/dist/conformance/index.d.ts +0 -80
  142. package/dist/conformance/index.js +0 -819
  143. package/dist/conformance/reach.d.ts +0 -10
  144. package/dist/conformance/reach.js +0 -72
  145. package/dist/conformance/store.d.ts +0 -5
  146. package/dist/conformance/store.js +0 -113
  147. package/dist/harbor/box.d.ts +0 -121
  148. package/dist/harbor/box.js +0 -121
  149. package/dist/harbor/core.d.ts +0 -55
  150. package/dist/harbor/core.js +0 -662
  151. package/dist/harbor/dial.d.ts +0 -9
  152. package/dist/harbor/dial.js +0 -81
  153. package/dist/harbor/memory.d.ts +0 -29
  154. package/dist/harbor/memory.js +0 -104
  155. package/dist/harbor/reach.d.ts +0 -36
  156. package/dist/harbor/reach.js +0 -199
  157. package/dist/harbor/store.d.ts +0 -35
  158. package/dist/harbor/store.js +0 -62
  159. package/dist/vector/cases.d.ts +0 -42
  160. package/dist/vector/cases.js +0 -223
  161. package/dist/vector/index.d.ts +0 -6
  162. package/dist/vector/index.js +0 -8
  163. package/dist/vector/stand.d.ts +0 -9
  164. package/dist/vector/stand.js +0 -77
  165. package/dist/vector/world.d.ts +0 -144
  166. package/dist/vector/world.js +0 -209
  167. package/dist/ward/arithmetic.d.ts +0 -31
  168. package/dist/ward/arithmetic.js +0 -249
  169. package/dist/ward/door.d.ts +0 -18
  170. package/dist/ward/door.js +0 -186
  171. package/dist/ward/ground.d.ts +0 -29
  172. package/dist/ward/ground.js +0 -56
  173. package/dist/ward/heirs.d.ts +0 -13
  174. package/dist/ward/heirs.js +0 -117
  175. package/dist/ward/json.d.ts +0 -2
  176. package/dist/ward/json.js +0 -163
  177. package/dist/ward/owner.d.ts +0 -13
  178. package/dist/ward/owner.js +0 -220
  179. package/dist/ward/seal.d.ts +0 -52
  180. package/dist/ward/seal.js +0 -150
  181. package/src/being/lent.ts +0 -72
  182. package/src/being/silence.ts +0 -46
  183. package/src/conformance/assert.ts +0 -100
  184. package/src/conformance/beings.ts +0 -188
  185. package/src/conformance/estate.ts +0 -412
  186. package/src/conformance/index.ts +0 -965
  187. package/src/conformance/reach.ts +0 -83
  188. package/src/conformance/store.ts +0 -125
  189. package/src/harbor/box.ts +0 -131
  190. package/src/harbor/core.ts +0 -699
  191. package/src/harbor/dial.ts +0 -112
  192. package/src/harbor/memory.ts +0 -123
  193. package/src/harbor/reach.ts +0 -221
  194. package/src/harbor/store.ts +0 -91
  195. package/src/vector/cases.ts +0 -257
  196. package/src/vector/index.ts +0 -11
  197. package/src/vector/stand.ts +0 -76
  198. package/src/vector/world.ts +0 -232
  199. package/src/ward/arithmetic.ts +0 -251
  200. package/src/ward/door.ts +0 -186
  201. package/src/ward/ground.ts +0 -142
  202. package/src/ward/heirs.ts +0 -116
  203. package/src/ward/json.ts +0 -144
  204. package/src/ward/owner.ts +0 -214
  205. package/src/ward/seal.ts +0 -178
package/README.md CHANGED
@@ -1,119 +1,84 @@
1
- # Nervur
1
+ # nervur
2
2
 
3
- Nervur is the first open source kit of Quo, the library itself: harbor, ward
4
- and being, with no dependencies.
3
+ Nervur's kit of [Quo](https://quo.systems). Quo is a protocol: an object
4
+ asks another object and gets an answer, without knowing where it is. This
5
+ package is one implementation of it, and the two names never stand for
6
+ the same thing. Where they disagree, Quo's spec wins.
5
7
 
6
- Quo is a protocol that lets an object ask another object and get an answer,
7
- without knowing whether that other object is in the same process, on the same
8
- device, or on another planet. Quo is the protocol and this is one
9
- implementation of it; the two names never stand for the same thing.
8
+ You write classes, beings and faculties, and nothing else. A harbor
9
+ unpacks a whole world from them.
10
10
 
11
- It is three words, two of which are beings, and two edges.
12
- Nothing else is Quo.
11
+ ## Install
13
12
 
14
- - **Harbor.** The program a device runs to boot wards. Owns the wire and the
15
- operating system. Not a being.
16
- - **Ward.** One process of its harbor. A being plus ward functions. Keeps
17
- beings and judges its door.
18
- - **Being.** One ordinary object, one voice.
19
-
20
- The truth is the spec, at <https://quo.systems/spec>, with the vectors it
21
- is proved by at <https://quo.systems/vectors>. It is the protocol alone:
22
- what any ward in any language must do for its bytes to be Quo. It is
23
- self-contained and assumes nothing from any other document, this README
24
- included. Read it first, at the source, which is Quo's and not this
25
- package's to carry.
26
-
27
- What this one kit chose, and another kit may refuse, is documented at
28
- <https://nervur.org/docs>. It assumes the spec, and where the two disagree
29
- the spec wins.
13
+ ```sh
14
+ npm install nervur
15
+ ```
30
16
 
31
- ## This package
17
+ Node 22.18 or later. The main entry imports no platform, so it runs in a
18
+ browser, Deno, Bun and workerd as well. `nervur/folder` keeps a harbor on
19
+ a disk and needs Node's file system.
32
20
 
33
- A TypeScript implementation, written against Node's own type stripping, with
34
- no dependencies. The package ships JavaScript with declarations beside the
35
- source it was emitted from, because Node strips types nowhere under
36
- `node_modules`.
21
+ ## A world in one process
37
22
 
38
- ```
39
- src/being/ the Being side: what a being author imports, if anything
40
- src/ward/ the ward: the Ground contract, door, seal, arithmetic, heirs, stance, allowance
41
- src/harbor/ MemoryHarbor, the box's being, and the harbor core with its store, reach and dialer
42
- src/conformance/ behaviours any ward must show, written against the truth
43
- src/vector/ vector mode, and the world the door's cases are taken in
23
+ ```js
24
+ import { Being, Faculty, World } from 'nervur';
25
+
26
+ // A contract, and a faculty that fulfils it.
27
+ class Clock extends Faculty {}
28
+ class SystemTime extends Clock {
29
+ static asks = { now: {} };
30
+ now() {
31
+ return { now: Date.now() };
32
+ }
33
+ }
34
+
35
+ // A being, who lends a clock by its contract.
36
+ class Greeter extends Being {
37
+ static asks = { hello: {} };
38
+ async hello(args, asker) {
39
+ if (!this.stance.standings.get('clock')) await this.stance.lend(Clock, 'clock');
40
+ const time = await this.stance.standings.get('clock')?.ask('now');
41
+ return { hello: asker.id ?? 'stranger', time };
42
+ }
43
+ }
44
+
45
+ const world = new World();
46
+ const harbor = await world.harbor('home', [SystemTime, Greeter]);
47
+ await harbor.root('open', { key: 'clock', class: 'SystemTime' });
48
+ await harbor.root('host', { ward: 'alice' });
49
+ await harbor.ward('alice').root('boot', { key: 'greeter', class: 'Greeter' });
44
50
  ```
45
51
 
46
- The suites live beside the source in the tree this is developed in and are
47
- not in the package: what proves the kit is not what a consumer installs.
48
- What does ship of them is `src/conformance/`, which is the part written for
49
- somebody else's ward rather than for this one.
52
+ A second harbor in the same world takes an invitation on the greeter and
53
+ asks it through the world, sealed as Quo's bytes. `world.restart('home')`
54
+ opens the harbor again from what its memory kept.
50
55
 
51
- ## Requirements
56
+ ## The onion
52
57
 
53
- Node 22.18 or later, and nothing else. The package has no dependencies, and
54
- it ships JavaScript with declarations, so nothing is compiled on the way in.
58
+ 1. The terrain gives the box ward's seed and memory.
59
+ 2. The box ward unpacks, its ward-being the dock.
60
+ 3. Its faculties are born from their rows.
61
+ 4. Every ward the dock hosts unpacks.
62
+ 5. In every ward, the ward-being first, then each being from her row.
55
63
 
56
- ## Where it is proven
64
+ Cells decide what stands. The classes you hand a harbor only resolve a
65
+ name a row keeps. Opening a harbor on empty memory is its genesis, and
66
+ the root's asks write the rest: `open`, `host`, and `boot` on a ward.
57
67
 
58
- The same conformance suite this package exports runs against several worlds:
59
- one ward, one harbor with a ward per being, two harbors, and the harbor core
60
- over a memory store. A world declares what it can express and a chapter it
61
- cannot reach is skipped by name rather than failed, which is how a kit in
62
- another language reports the same suite honestly.
68
+ ## What it holds
63
69
 
64
- It is run again out of a bundle in a real Chromium, in workerd, in Deno and
65
- in Bun, because a ward's truth must hold wherever a ward runs.
66
-
67
- ## Entry points
68
-
69
- ```js
70
- import { Being, silence, digest } from 'nervur';
71
- import { Ward } from 'nervur/ward';
72
- import { MemoryHarbor, Harbor, request, dial } from 'nervur/harbor';
73
- import { conform } from 'nervur/conformance';
74
- import { Stand } from 'nervur/vector';
75
- ```
70
+ - **Being, Faculty.** What you extend. A faculty's contract is its class
71
+ and every class between it and `Faculty`.
72
+ - **Entropy, Clock, Custody, Memory, Carrier.** The contracts a terrain
73
+ fulfils. Each has one suite, and every body that fulfils it passes it.
74
+ - **Harbor, Terrain, Registry, Dock, WardBeing.** The onion.
75
+ - **World, PointerTerrain** and the pointer bodies. A whole world with
76
+ this package alone.
77
+ - **FolderMemory, FolderCustody**, from `nervur/folder`.
76
78
 
77
- ## Another language
78
-
79
- The hand to a kit in another language is Quo's own and not this package's.
80
- The spec is the protocol and assumes nothing: a kit is written against it
81
- and against nothing else. The vectors beside it are the byte-level hand,
82
- everything a stranger can observe: `arithmetic.json`, the primitives the
83
- seal rests on, SHA-256, Ed25519, X25519, HKDF and AES-256-GCM;
84
- `framing.json`, Quo's own, the ward pk, the digest, the signed ask body,
85
- the sealed shapes, the invitation and the knock; `wire.json`, the frames on
86
- a socket and the one request a door takes; and `door.json`, the door's
87
- thirteen cases, each one an arrival a ward will not answer, with the bytes
88
- that arrive, the bytes that leave and the partition's digest on both sides
89
- of the judgement. All of it is at <https://quo.systems>, downloadable and
90
- versionless, and a kit reproduces the bytes or it is not this protocol.
91
-
92
- `door.json` is also replayed from outside, by a verifier holding no key,
93
- against a kit standing in vector mode, which the spec describes under that
94
- name. This kit stands in it with `Stand` from `nervur/vector`, which is
95
- vector mode and names no runtime: put it behind any listener that hands it
96
- a request body and returns what it answers.
97
-
98
- Nothing in this package is the protocol. `src/` is this kit's
99
- interpretation, and `src/conformance/` is a checklist a kit ports rather
100
- than a harness it runs: it imports the base class, the silence spelling and
101
- this kit's store and reach, so the beings it is shown with run only in a
102
- TypeScript ward.
103
-
104
- ## Versions
105
-
106
- No compatibility promise before 1.0.0: the words may still move, and there
107
- is no migration to write because there is nothing yet to migrate from. This
108
- package carries the version of its own work and is bound to no other's, so a
109
- number equal to `@nervur-org/dock`'s or `@nervur-org/ui`'s is a coincidence.
110
-
111
- What ships is the emitted `dist/`, the source it came from, this file, the
112
- licence and the notice. No tests and no configs. The spec and the vectors
113
- are not in the tarball: they are Quo's and not this package's to carry, and
114
- they stand at <https://quo.systems>, which is where this file points.
79
+ Carriers over a network, storage beyond a folder, key custody, and every
80
+ screen are `@nervur-org/dock`'s, as classes fulfilling these contracts.
115
81
 
116
82
  ## License
117
83
 
118
- Apache-2.0. Copyright 2026 Razvan Gherghina. See [LICENSE](LICENSE) and
119
- [NOTICE](NOTICE); every source file carries an SPDX line.
84
+ Apache-2.0.
@@ -1,24 +1,16 @@
1
- import type { Asker, Blueprint, Cells, Invitation, JsonObject, Wanted, OccupantRecord, Occupants, Reply, Schema, Stance, Standings, Answer } from './types.ts';
1
+ import type { Asker, Blueprint, JsonObject, Reply, Schema, Stance } from './types.ts';
2
2
  export type AskSpec = {
3
- description?: string;
4
- input?: Schema;
5
- output?: Schema;
6
- for?: (occupant: OccupantRecord | undefined, asker: Asker) => boolean;
3
+ readonly description?: string;
4
+ readonly input?: Schema;
5
+ readonly for?: (asker: Asker, notes: JsonObject | undefined) => boolean;
7
6
  };
8
7
  export declare class Being {
9
8
  static cells: JsonObject;
10
9
  static asks: Record<string, AskSpec>;
11
10
  readonly stance: Stance;
12
11
  constructor(stance: Stance);
13
- get cells(): Cells;
14
- get standings(): Standings;
15
- get occupants(): Occupants;
16
- lend(name: string, id: string): Promise<string | null>;
17
- invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
18
- knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
19
- take(id: string, invitation: Invitation): Promise<string | null>;
20
- boot(className: string, key: string, id?: string): Promise<string | null>;
21
- occupant(asker: Asker): OccupantRecord | undefined;
12
+ get cells(): JsonObject;
13
+ notes(asker: Asker): JsonObject | undefined;
22
14
  describe(asker: Asker): Blueprint;
23
- answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
15
+ answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply>;
24
16
  }
@@ -1,10 +1,6 @@
1
- // Names a subclass may not use for an ask, because they are the base's own.
2
- const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'lend', 'constructor']);
3
- // Whether she has a method of that name, written on her own prototype chain
4
- // below Object's. A name Object lends every object, `hasOwnProperty` or
5
- // `toString`, is not a method she wrote; and a field she assigns in her own
6
- // constructor is not there yet when the base checks, so an ask is a method
7
- // on the prototype and nothing else.
1
+ // Names a subclass may not declare as an ask, because they are the base's.
2
+ const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'notes', 'constructor']);
3
+ // Whether she wrote a method of that name, below Object's prototype.
8
4
  const wrote = (self, name) => {
9
5
  for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
10
6
  if (Object.hasOwn(p, name))
@@ -12,18 +8,11 @@ const wrote = (self, name) => {
12
8
  }
13
9
  return false;
14
10
  };
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: {} }`.
11
+ // A subclass's statics replace its parent's and never merge: her blueprint
12
+ // is what the class in front of you declares, in its order.
21
13
  export class Being {
22
- // Her cells' defaults. Merged in at birth, only where a key is missing, so
23
- // a restart keeps what she wrote.
14
+ // Her cells' defaults, written at birth only where a key is missing.
24
15
  static cells = {};
25
- // What she can be asked. Declaration order is blueprint order, except a
26
- // name that reads as an array index, which the language lists first.
27
16
  static asks = {};
28
17
  stance;
29
18
  constructor(stance) {
@@ -33,10 +22,8 @@ export class Being {
33
22
  if (RESERVED.has(name))
34
23
  throw new Error(`ask '${name}' is a reserved name`);
35
24
  if (!wrote(this, name))
36
- throw new Error(`ask '${name}' has no method on the prototype`);
25
+ throw new Error(`ask '${name}' has no method`);
37
26
  }
38
- // Own keys only: a default named after a member of Object's prototype is
39
- // still hers, and still missing until she writes it.
40
27
  for (const [k, v] of Object.entries(C.cells))
41
28
  if (!Object.hasOwn(stance.cells, k))
42
29
  stance.cells[k] = structuredClone(v);
@@ -44,66 +31,28 @@ export class Being {
44
31
  get cells() {
45
32
  return this.stance.cells;
46
33
  }
47
- get standings() {
48
- return this.stance.standings;
34
+ // The notes she invited this asker under.
35
+ notes(asker) {
36
+ return asker.id === undefined ? undefined : this.stance.occupants.notes(asker.id);
49
37
  }
50
- get occupants() {
51
- return this.stance.occupants;
52
- }
53
- // A standing at one of the things this device can do, under an id of hers.
54
- // The ward knocks and takes it for her; the invitation never reaches her.
55
- lend(name, id) {
56
- return this.stance.lend(name, id);
57
- }
58
- invite(id, notes) {
59
- return this.stance.occupants.invite(id, notes);
60
- }
61
- knock(invitation, method, args, wanted) {
62
- return this.stance.standings.knock(invitation, method, args, wanted);
63
- }
64
- take(id, invitation) {
65
- return this.stance.standings.take(id, invitation);
66
- }
67
- // A new being of her ward, by class name, under a key she chooses. With an
68
- // id, she holds a standing to the being she made, who knows her by her key.
69
- boot(className, key, id) {
70
- return this.stance.boot(className, key, id);
71
- }
72
- // The occupant record for whoever is at the door. Undefined at a public being.
73
- occupant(asker) {
74
- return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
75
- }
76
- // Her blueprint for this asker. Override to shape it by hand.
77
38
  describe(asker) {
78
39
  const C = this.constructor;
79
- const rec = this.occupant(asker);
40
+ const notes = this.notes(asker);
80
41
  const asks = [];
81
42
  for (const [name, spec] of Object.entries(C.asks)) {
82
- if (spec.for && !spec.for(rec, asker))
43
+ if (spec.for && !spec.for(asker, notes))
83
44
  continue;
84
- const ask = { name, input: spec.input ?? { type: 'object' } };
85
- if (spec.description !== undefined)
86
- ask.description = spec.description;
87
- if (spec.output !== undefined)
88
- ask.output = spec.output;
89
- asks.push(ask);
45
+ asks.push({ name, ...(spec.description === undefined ? {} : { description: spec.description }), input: spec.input ?? { type: 'object' } });
90
46
  }
91
47
  return { asks, notes: {} };
92
48
  }
93
- // The one function. Override to wrap it; call super to keep the dispatch.
94
- async answer(asker, method, args = {}) {
49
+ async answer(asker, method, args) {
95
50
  if (method === undefined)
96
51
  return this.describe(asker);
97
52
  const C = this.constructor;
98
- // Declared, by her, on purpose. `asks` is an ordinary object, so a bare
99
- // lookup would also find every name on Object's prototype: `valueOf`
100
- // would answer with her stance, `toString` with a string, and neither is
101
- // an ask she wrote. Only her own keys are asks, which is what describe
102
- // shows. What she shows is what she can be asked.
103
53
  const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
104
- if (!spec || (spec.for && !spec.for(this.occupant(asker), asker)))
54
+ if (!spec || (spec.for && !spec.for(asker, this.notes(asker))))
105
55
  return { error: 'unknown ask' };
106
- const fn = this[method];
107
- return fn.call(this, args ?? {}, asker);
56
+ return this[method].call(this, args, asker);
108
57
  }
109
58
  }
@@ -1,3 +1,3 @@
1
- import type { Json } from './types.ts';
1
+ import { type Json } from '../crypto/index.ts';
2
2
  export declare const canonical: (v: Json) => string;
3
3
  export declare const digest: (blueprint: Json) => Promise<string>;
@@ -1,37 +1,17 @@
1
- // A value I-JSON has no room for: a key left empty, a function, a symbol.
2
- // None of them cross an edge, so none of them may reach a digest. A key
3
- // carrying one is dropped and a slot carrying one is null, which is what
4
- // crossing does to them, so the digest names what arrived and not what she
5
- // happened to be holding.
6
- const absent = (v) => v === undefined || typeof v === 'function' || typeof v === 'symbol';
7
- // JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
8
- // are serialized as ES does, which is what RFC 8785 specifies. A hole in a
9
- // list is null, as JSON writes it.
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A being's `seen`: SHA-256, lowercase hex, over the canonical form of her
3
+ // blueprint for one asker. Keys sorted by UTF-16 code units, numbers as
4
+ // ECMAScript writes them, no whitespace.
5
+ import { hex, sha256, utf8 } from '../crypto/index.js';
10
6
  export const canonical = (v) => {
11
7
  if (Array.isArray(v))
12
- return `[${Array.from(v, (slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`;
8
+ return `[${v.map(canonical).join(',')}]`;
13
9
  if (v !== null && typeof v === 'object') {
14
10
  return `{${Object.keys(v)
15
- .filter((k) => !absent(v[k]))
16
11
  .sort()
17
12
  .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
18
13
  .join(',')}}`;
19
14
  }
20
15
  return JSON.stringify(v);
21
16
  };
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.
26
- const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
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))));
17
+ export const digest = async (blueprint) => hex(await sha256(utf8(canonical(blueprint))));
@@ -0,0 +1,9 @@
1
+ import { Being } from './being.ts';
2
+ import { type Asker, type Blueprint, type JsonObject, type Reply } from './types.ts';
3
+ export declare class Faculty extends Being {
4
+ #private;
5
+ static contracts(C: abstract new (...args: never[]) => unknown): string[];
6
+ static fulfils(C: unknown): C is typeof Faculty;
7
+ describe(asker: Asker): Blueprint;
8
+ answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply>;
9
+ }
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A faculty: a being of the box ward that fulfils a contract. A contract is
3
+ // a class between `Faculty` and the class that stands, usually abstract:
4
+ //
5
+ // abstract class Timer extends Faculty { ... } the contract
6
+ // class IntervalTimer extends Timer { ... } one class that fulfils it
7
+ //
8
+ // A being lends by the contract and never learns the class. Two asks are the
9
+ // faculty's own and answered to the ward-being that opened her alone:
10
+ // `offer`, an invitation on herself for a ward the dock vouches for, and
11
+ // `retract`, an offer that was not taken.
12
+ import { Being } from './being.js';
13
+ import { WARD } from './types.js';
14
+ export class Faculty extends Being {
15
+ // The contracts a class fulfils: itself and every class between it and
16
+ // `Faculty`, by name.
17
+ static contracts(C) {
18
+ const names = [];
19
+ for (let c = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c))
20
+ names.push(c.name);
21
+ return names;
22
+ }
23
+ static fulfils(C) {
24
+ return typeof C === 'function' && C.prototype instanceof Faculty;
25
+ }
26
+ #open() {
27
+ if (this.cells.offers === undefined)
28
+ this.cells.offers = {};
29
+ return this.cells.offers;
30
+ }
31
+ async #offer() {
32
+ const n = (this.cells.offered ?? 0) + 1;
33
+ this.cells.offered = n;
34
+ const id = `lent:${n}`;
35
+ const invitation = await this.stance.occupants.invite(id);
36
+ if (invitation === null)
37
+ return { error: 'not invited' };
38
+ this.#open()[invitation.heir] = id;
39
+ return { invitation };
40
+ }
41
+ #retract(args) {
42
+ const open = this.#open();
43
+ const id = typeof args.heir === 'string' && Object.hasOwn(open, args.heir) ? open[args.heir] : undefined;
44
+ if (id === undefined)
45
+ return { retracted: null };
46
+ this.stance.occupants.remove(id);
47
+ delete open[args.heir];
48
+ return { retracted: id };
49
+ }
50
+ describe(asker) {
51
+ const blueprint = super.describe(asker);
52
+ if (asker.id !== WARD)
53
+ return blueprint;
54
+ return { ...blueprint, asks: [...blueprint.asks, { name: 'offer', input: { type: 'object' } }, { name: 'retract', input: { type: 'object', required: ['heir'] } }] };
55
+ }
56
+ // An offer is closed by the first word from the occupant it was made for.
57
+ answer(asker, method, args) {
58
+ if (asker.id === WARD && method === 'offer')
59
+ return this.#offer();
60
+ if (asker.id === WARD && method === 'retract')
61
+ return Promise.resolve(this.#retract(args));
62
+ const open = this.#open();
63
+ for (const [heir, id] of Object.entries(open))
64
+ if (id === asker.id)
65
+ delete open[heir];
66
+ return super.answer(asker, method, args);
67
+ }
68
+ }
@@ -1,6 +1,5 @@
1
1
  export { Being, type AskSpec } from './being.ts';
2
- export { Lent, BOX, LENT_ASKS } from './lent.ts';
3
- export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
4
- export { digest, canonical } from './digest.ts';
5
- export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.ts';
6
- export type * from './types.ts';
2
+ export { canonical, digest } from './digest.ts';
3
+ export { Faculty } from './faculty.ts';
4
+ export { OWNER, WARD, type Answer, type Ask, type Asker, type BeingClass, type BeingLike, type Blueprint, type Invitation, type Json, type JsonObject, type Occupants, type Reply, type Schema, type Stance, type Standing, type Standings, type Wanted } from './types.ts';
5
+ export { isSilence, isWord, silence, told, word, type Silence, type Word, type WordName } from './words.ts';
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // nervur — the Being side. What a being author imports, if anything.
2
+ // What a being author imports.
3
3
  export { Being } from './being.js';
4
- export { Lent, BOX, LENT_ASKS } from './lent.js';
5
- export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.js';
6
- export { digest, canonical } from './digest.js';
7
- export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.js';
4
+ export { canonical, digest } from './digest.js';
5
+ export { Faculty } from './faculty.js';
6
+ export { OWNER, WARD } from './types.js';
7
+ export { isSilence, isWord, silence, told, word } from './words.js';
@@ -1,89 +1,62 @@
1
- import type { silence } from './silence.ts';
2
- export type Json = null | boolean | number | string | Json[] | {
3
- [key: string]: Json;
4
- };
1
+ import type { Json } from '../crypto/index.ts';
2
+ import type { Invitation } from '../quo/index.ts';
3
+ import type { Silence, Word } from './words.ts';
4
+ export type { Invitation, Json };
5
5
  export type JsonObject = {
6
6
  [key: string]: Json;
7
7
  };
8
8
  export type Asker = {
9
- id: string;
9
+ readonly id: string;
10
10
  } | {
11
- id?: undefined;
11
+ readonly id?: undefined;
12
12
  };
13
13
  export declare const OWNER = "OWNER";
14
- export declare const PUBLIC = "PUBLIC";
15
- export declare const RESERVED_IDS: readonly string[];
16
- export type Invitation = {
17
- ward: string;
18
- heir?: string;
19
- secret?: string;
14
+ export declare const WARD = "ward";
15
+ export type Answer = Json | Silence | Word;
16
+ export type Reply = Json | Silence | undefined;
17
+ export type Wanted = {
18
+ readonly time?: number;
20
19
  };
21
- export declare const invitationArgs: (inv: Invitation) => JsonObject;
22
- export declare const isInvitation: (v: unknown) => v is Invitation;
23
20
  export type Schema = JsonObject;
24
21
  export type Ask = {
25
22
  name: string;
26
23
  description?: string;
27
24
  input: Schema;
28
- output?: Schema;
29
25
  };
30
26
  export type Blueprint = {
31
27
  asks: Ask[];
32
28
  notes: Json;
33
29
  };
34
- export declare const isBlueprint: (v: unknown) => v is Blueprint;
35
- export type StandingRecord = {
36
- id: string;
37
- digest: string | null;
38
- blueprint: Blueprint | null;
39
- seen: string | null;
40
- };
41
- export type OccupantRecord = {
42
- id: string;
43
- notes: JsonObject;
44
- };
45
- export type Cells = {
46
- standings: Record<string, StandingRecord>;
47
- occupants: Record<string, OccupantRecord>;
48
- [hers: string]: Json;
49
- };
50
- export type Silence = typeof silence;
51
- export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
52
- export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
53
- export type WordName = DoorWord | WardWord;
54
- export declare const WORD_KEY: unique symbol;
55
- export type Word<W extends WordName = WordName> = {
56
- readonly [K in typeof WORD_KEY]: W;
57
- };
58
- export type Unreached = Word<'unreached'>;
59
- export type Answer = Json | Silence | Word;
60
- export type Reply = Json | Silence;
61
- export type Wanted = {
62
- time?: number;
63
- };
64
- export type Standing = {
30
+ export interface Standing {
65
31
  readonly id: string;
66
32
  ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
67
- };
68
- export type Standings = {
69
- knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
33
+ }
34
+ export interface Standings {
70
35
  take(id: string, invitation: Invitation): Promise<string | null>;
71
- remove(id: string): void;
72
- } & {
73
- readonly [id: string]: Standing | undefined;
74
- };
75
- export type Occupants = {
36
+ get(id: string): Standing | undefined;
37
+ ids(): string[];
38
+ remove(id: string): boolean;
39
+ }
40
+ export interface Occupants {
76
41
  invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
77
- remove(id: string): void;
78
- };
79
- export type Stance = {
80
- readonly cells: Cells;
42
+ notes(id: string): JsonObject | undefined;
43
+ ids(): string[];
44
+ remove(id: string): boolean;
45
+ }
46
+ export interface Stance {
47
+ readonly key: string;
48
+ readonly cells: JsonObject;
81
49
  readonly occupants: Occupants;
82
50
  readonly standings: Standings;
83
- lend(name: string, id: string): Promise<string | null>;
84
51
  boot(className: string, key: string, id?: string): Promise<string | null>;
85
- };
52
+ lend(contract: {
53
+ readonly name: string;
54
+ }, id: string): Promise<string | null>;
55
+ }
86
56
  export interface BeingLike {
87
- answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
57
+ answer(asker: Asker, method: string | undefined, args: JsonObject): Reply | Promise<Reply>;
58
+ describe?(asker: Asker): Blueprint;
88
59
  }
89
- export type BeingClass = new (stance: Stance) => BeingLike;
60
+ export type BeingClass = (new (stance: Stance) => BeingLike) & {
61
+ readonly name: string;
62
+ };