nervur 0.19.2 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/README.md +156 -104
  2. package/dist/being/being.d.ts +8 -15
  3. package/dist/being/being.js +20 -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 +10 -0
  7. package/dist/being/faculty.js +74 -0
  8. package/dist/being/index.d.ts +5 -5
  9. package/dist/being/index.js +6 -5
  10. package/dist/being/kind.d.ts +4 -0
  11. package/dist/being/kind.js +28 -0
  12. package/dist/being/types.d.ts +35 -63
  13. package/dist/being/types.js +3 -57
  14. package/dist/being/words.d.ts +13 -0
  15. package/dist/being/words.js +14 -0
  16. package/dist/cli/command.d.ts +13 -0
  17. package/dist/cli/command.js +194 -0
  18. package/dist/cli/harbor.d.ts +21 -0
  19. package/dist/cli/harbor.js +141 -0
  20. package/dist/cli/main.d.ts +2 -0
  21. package/dist/cli/main.js +14 -0
  22. package/dist/contract/index.d.ts +39 -0
  23. package/dist/contract/index.js +33 -0
  24. package/dist/crypto/aes.d.ts +3 -0
  25. package/dist/crypto/aes.js +24 -0
  26. package/dist/crypto/bytes.d.ts +6 -0
  27. package/dist/crypto/bytes.js +33 -0
  28. package/dist/crypto/ed25519.d.ts +3 -0
  29. package/dist/crypto/ed25519.js +85 -0
  30. package/dist/crypto/hash.d.ts +2 -0
  31. package/dist/crypto/hash.js +11 -0
  32. package/dist/crypto/index.d.ts +7 -0
  33. package/dist/crypto/index.js +10 -0
  34. package/dist/crypto/json.d.ts +8 -0
  35. package/dist/crypto/json.js +250 -0
  36. package/dist/crypto/mlkem.d.ts +12 -0
  37. package/dist/crypto/mlkem.js +36 -0
  38. package/dist/crypto/subtle.d.ts +10 -0
  39. package/dist/crypto/subtle.js +29 -0
  40. package/dist/crypto/x25519.d.ts +2 -0
  41. package/dist/crypto/x25519.js +21 -0
  42. package/dist/folder/index.d.ts +14 -0
  43. package/dist/folder/index.js +87 -0
  44. package/dist/harbor/catalogue.d.ts +25 -0
  45. package/dist/harbor/catalogue.js +93 -0
  46. package/dist/harbor/dock.d.ts +29 -0
  47. package/dist/harbor/dock.js +122 -0
  48. package/dist/harbor/harbor.d.ts +31 -0
  49. package/dist/harbor/harbor.js +146 -0
  50. package/dist/harbor/index.d.ts +7 -10
  51. package/dist/harbor/index.js +9 -18
  52. package/dist/harbor/package.d.ts +10 -0
  53. package/dist/harbor/package.js +115 -0
  54. package/dist/harbor/registry.d.ts +9 -0
  55. package/dist/harbor/registry.js +78 -0
  56. package/dist/harbor/root-line.d.ts +9 -0
  57. package/dist/harbor/root-line.js +36 -0
  58. package/dist/harbor/terrain.d.ts +9 -0
  59. package/dist/harbor/terrain.js +2 -0
  60. package/dist/index.d.ts +7 -0
  61. package/dist/index.js +14 -0
  62. package/dist/pointer/bodies.d.ts +26 -0
  63. package/dist/pointer/bodies.js +64 -0
  64. package/dist/pointer/index.d.ts +2 -0
  65. package/dist/pointer/index.js +4 -0
  66. package/dist/pointer/world.d.ts +35 -0
  67. package/dist/pointer/world.js +85 -0
  68. package/dist/quo/door.d.ts +34 -0
  69. package/dist/quo/door.js +172 -0
  70. package/dist/quo/index.d.ts +8 -0
  71. package/dist/quo/index.js +11 -0
  72. package/dist/quo/invitation.d.ts +7 -0
  73. package/dist/quo/invitation.js +15 -0
  74. package/dist/quo/keys.d.ts +40 -0
  75. package/dist/quo/keys.js +79 -0
  76. package/dist/quo/payload.d.ts +15 -0
  77. package/dist/quo/payload.js +55 -0
  78. package/dist/quo/relations.d.ts +40 -0
  79. package/dist/quo/relations.js +33 -0
  80. package/dist/quo/reply.d.ts +13 -0
  81. package/dist/quo/reply.js +35 -0
  82. package/dist/quo/seal.d.ts +42 -0
  83. package/dist/quo/seal.js +78 -0
  84. package/dist/quo/standing.d.ts +39 -0
  85. package/dist/quo/standing.js +90 -0
  86. package/dist/tcp/frame.d.ts +29 -0
  87. package/dist/tcp/frame.js +74 -0
  88. package/dist/tcp/index.d.ts +18 -0
  89. package/dist/tcp/index.js +164 -0
  90. package/dist/ward/allowance.d.ts +13 -9
  91. package/dist/ward/allowance.js +28 -67
  92. package/dist/ward/cells.d.ts +6 -6
  93. package/dist/ward/cells.js +67 -179
  94. package/dist/ward/index.d.ts +6 -10
  95. package/dist/ward/index.js +7 -13
  96. package/dist/ward/partition.d.ts +44 -60
  97. package/dist/ward/partition.js +56 -285
  98. package/dist/ward/pilot.d.ts +8 -0
  99. package/dist/ward/pilot.js +50 -0
  100. package/dist/ward/stance.d.ts +27 -20
  101. package/dist/ward/stance.js +174 -406
  102. package/dist/ward/ward-being.d.ts +35 -0
  103. package/dist/ward/ward-being.js +82 -0
  104. package/dist/ward/ward.d.ts +45 -11
  105. package/dist/ward/ward.js +239 -357
  106. package/package.json +24 -31
  107. package/src/being/being.ts +35 -78
  108. package/src/being/digest.ts +7 -35
  109. package/src/being/faculty.ts +73 -0
  110. package/src/being/index.ts +6 -6
  111. package/src/being/kind.ts +30 -0
  112. package/src/being/types.ts +50 -146
  113. package/src/being/words.ts +31 -0
  114. package/src/cli/command.ts +188 -0
  115. package/src/cli/harbor.ts +137 -0
  116. package/src/cli/main.ts +13 -0
  117. package/src/contract/index.ts +73 -0
  118. package/src/crypto/aes.ts +26 -0
  119. package/src/crypto/bytes.ts +37 -0
  120. package/src/crypto/ed25519.ts +84 -0
  121. package/src/crypto/hash.ts +14 -0
  122. package/src/crypto/index.ts +10 -0
  123. package/src/crypto/json.ts +241 -0
  124. package/src/crypto/mlkem.ts +38 -0
  125. package/src/crypto/subtle.ts +33 -0
  126. package/src/crypto/x25519.ts +21 -0
  127. package/src/folder/index.ts +92 -0
  128. package/src/harbor/catalogue.ts +110 -0
  129. package/src/harbor/dock.ts +137 -0
  130. package/src/harbor/harbor.ts +180 -0
  131. package/src/harbor/index.ts +9 -19
  132. package/src/harbor/package.ts +131 -0
  133. package/src/harbor/registry.ts +73 -0
  134. package/src/harbor/root-line.ts +36 -0
  135. package/src/harbor/terrain.ts +15 -0
  136. package/src/index.ts +20 -0
  137. package/src/pointer/bodies.ts +65 -0
  138. package/src/pointer/index.ts +4 -0
  139. package/src/pointer/world.ts +94 -0
  140. package/src/quo/door.ts +178 -0
  141. package/src/quo/index.ts +11 -0
  142. package/src/quo/invitation.ts +15 -0
  143. package/src/quo/keys.ts +91 -0
  144. package/src/quo/payload.ts +61 -0
  145. package/src/quo/relations.ts +62 -0
  146. package/src/quo/reply.ts +38 -0
  147. package/src/quo/seal.ts +101 -0
  148. package/src/quo/standing.ts +111 -0
  149. package/src/stand/main.ts +20 -0
  150. package/src/stand/stand.ts +220 -0
  151. package/src/tcp/frame.ts +78 -0
  152. package/src/tcp/index.ts +173 -0
  153. package/src/ward/allowance.ts +37 -75
  154. package/src/ward/cells.ts +63 -176
  155. package/src/ward/index.ts +7 -17
  156. package/src/ward/partition.ts +86 -326
  157. package/src/ward/pilot.ts +50 -0
  158. package/src/ward/stance.ts +186 -420
  159. package/src/ward/ward-being.ts +103 -0
  160. package/src/ward/ward.ts +256 -359
  161. package/dist/being/lent.d.ts +0 -32
  162. package/dist/being/lent.js +0 -72
  163. package/dist/being/silence.d.ts +0 -12
  164. package/dist/being/silence.js +0 -41
  165. package/dist/conformance/assert.d.ts +0 -11
  166. package/dist/conformance/assert.js +0 -106
  167. package/dist/conformance/beings.d.ts +0 -199
  168. package/dist/conformance/beings.js +0 -188
  169. package/dist/conformance/estate.d.ts +0 -5
  170. package/dist/conformance/estate.js +0 -388
  171. package/dist/conformance/index.d.ts +0 -80
  172. package/dist/conformance/index.js +0 -819
  173. package/dist/conformance/reach.d.ts +0 -10
  174. package/dist/conformance/reach.js +0 -72
  175. package/dist/conformance/store.d.ts +0 -5
  176. package/dist/conformance/store.js +0 -113
  177. package/dist/harbor/box.d.ts +0 -121
  178. package/dist/harbor/box.js +0 -121
  179. package/dist/harbor/core.d.ts +0 -55
  180. package/dist/harbor/core.js +0 -662
  181. package/dist/harbor/dial.d.ts +0 -9
  182. package/dist/harbor/dial.js +0 -81
  183. package/dist/harbor/memory.d.ts +0 -29
  184. package/dist/harbor/memory.js +0 -104
  185. package/dist/harbor/reach.d.ts +0 -36
  186. package/dist/harbor/reach.js +0 -199
  187. package/dist/harbor/store.d.ts +0 -35
  188. package/dist/harbor/store.js +0 -62
  189. package/dist/vector/cases.d.ts +0 -42
  190. package/dist/vector/cases.js +0 -223
  191. package/dist/vector/index.d.ts +0 -6
  192. package/dist/vector/index.js +0 -8
  193. package/dist/vector/stand.d.ts +0 -9
  194. package/dist/vector/stand.js +0 -77
  195. package/dist/vector/world.d.ts +0 -144
  196. package/dist/vector/world.js +0 -209
  197. package/dist/ward/arithmetic.d.ts +0 -31
  198. package/dist/ward/arithmetic.js +0 -249
  199. package/dist/ward/door.d.ts +0 -18
  200. package/dist/ward/door.js +0 -186
  201. package/dist/ward/ground.d.ts +0 -29
  202. package/dist/ward/ground.js +0 -56
  203. package/dist/ward/heirs.d.ts +0 -13
  204. package/dist/ward/heirs.js +0 -117
  205. package/dist/ward/json.d.ts +0 -2
  206. package/dist/ward/json.js +0 -163
  207. package/dist/ward/owner.d.ts +0 -13
  208. package/dist/ward/owner.js +0 -220
  209. package/dist/ward/seal.d.ts +0 -52
  210. package/dist/ward/seal.js +0 -150
  211. package/src/being/lent.ts +0 -72
  212. package/src/being/silence.ts +0 -46
  213. package/src/conformance/assert.ts +0 -100
  214. package/src/conformance/beings.ts +0 -188
  215. package/src/conformance/estate.ts +0 -412
  216. package/src/conformance/index.ts +0 -965
  217. package/src/conformance/reach.ts +0 -83
  218. package/src/conformance/store.ts +0 -125
  219. package/src/harbor/box.ts +0 -131
  220. package/src/harbor/core.ts +0 -699
  221. package/src/harbor/dial.ts +0 -112
  222. package/src/harbor/memory.ts +0 -123
  223. package/src/harbor/reach.ts +0 -221
  224. package/src/harbor/store.ts +0 -91
  225. package/src/vector/cases.ts +0 -257
  226. package/src/vector/index.ts +0 -11
  227. package/src/vector/stand.ts +0 -76
  228. package/src/vector/world.ts +0 -232
  229. package/src/ward/arithmetic.ts +0 -251
  230. package/src/ward/door.ts +0 -186
  231. package/src/ward/ground.ts +0 -142
  232. package/src/ward/heirs.ts +0 -116
  233. package/src/ward/json.ts +0 -144
  234. package/src/ward/owner.ts +0 -214
  235. package/src/ward/seal.ts +0 -178
package/package.json CHANGED
@@ -1,54 +1,47 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.19.2",
4
- "description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is. Being, Ward, Harbor.",
3
+ "version": "0.20.1",
4
+ "description": "Nervur's kit of Quo: an object asks another object and gets an answer, without knowing where it is.",
5
5
  "keywords": [
6
6
  "quo",
7
7
  "protocol",
8
8
  "capability",
9
- "object-capability",
10
- "rpc",
11
- "ed25519",
12
- "x25519"
9
+ "object-capability"
13
10
  ],
14
11
  "author": "Razvan Gherghina",
15
12
  "license": "Apache-2.0",
16
13
  "homepage": "https://nervur.org",
17
14
  "type": "module",
18
- "engines": {
19
- "node": ">=22.18"
15
+ "bin": {
16
+ "nervur": "dist/cli/main.js"
20
17
  },
21
18
  "exports": {
22
19
  ".": {
23
- "types": "./dist/being/index.d.ts",
24
- "default": "./dist/being/index.js"
25
- },
26
- "./ward": {
27
- "types": "./dist/ward/index.d.ts",
28
- "default": "./dist/ward/index.js"
29
- },
30
- "./harbor": {
31
- "types": "./dist/harbor/index.d.ts",
32
- "default": "./dist/harbor/index.js"
33
- },
34
- "./conformance": {
35
- "types": "./dist/conformance/index.d.ts",
36
- "default": "./dist/conformance/index.js"
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
37
22
  },
38
- "./vector": {
39
- "types": "./dist/vector/index.d.ts",
40
- "default": "./dist/vector/index.js"
23
+ "./folder": {
24
+ "types": "./dist/folder/index.d.ts",
25
+ "default": "./dist/folder/index.js"
41
26
  },
42
- "./package.json": "./package.json"
27
+ "./tcp": {
28
+ "types": "./dist/tcp/index.d.ts",
29
+ "default": "./dist/tcp/index.js"
30
+ }
43
31
  },
44
32
  "scripts": {
45
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
46
- "check": "node ../../test.mjs \"test/*.test.ts\"",
47
- "experimental": "node ../../test.mjs \"experimental/*.test.ts\"",
48
- "deep": "node --test --test-timeout=300000 \"test/terrain/*.test.ts\"",
49
- "cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/*.test.ts\"",
33
+ "build": "tsc -p tsconfig.build.json",
34
+ "check": "tsc -p tsconfig.json && npm run build && node ../../test.mjs \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
35
+ "deep": "node ../../test.mjs \"test/terrain/*.test.ts\"",
36
+ "cover": "node --test --test-timeout=60000 --experimental-test-coverage --test-coverage-exclude=\"**/dist/**\" --test-coverage-exclude=\"**/test/**\" --test-coverage-exclude=\"**/src/stand/**\" --test-coverage-exclude=\"../../quo/**\" --test-coverage-lines=99 --test-coverage-branches=93 --test-coverage-functions=95 \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
50
37
  "prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
51
38
  },
39
+ "engines": {
40
+ "node": ">=22.18"
41
+ },
42
+ "dependencies": {
43
+ "@noble/post-quantum": "0.7.1"
44
+ },
52
45
  "publishConfig": {
53
46
  "access": "public"
54
47
  },
@@ -1,28 +1,23 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The base class most beings extend. It writes `answer` for you: the empty
3
- // ask becomes a blueprint from the asks you declare, filtered per asker; a
4
- // named ask becomes a method call; anything not declared, or hidden from
5
- // this asker, is an error object. Underneath it is still one function, and
6
- // a class that writes that function by hand is a being just the same.
7
- import type { Ask, Asker, Blueprint, Cells, Invitation, JsonObject, Wanted, OccupantRecord, Occupants, Reply, Schema, Stance, Standings, Answer } from './types.ts';
2
+ // The base class most beings extend. It writes `answer`: the empty ask is
3
+ // her blueprint for this asker, a named ask calls her method of that name,
4
+ // and an ask she did not declare, or hides from this asker, is an error
5
+ // object. A class that writes `answer` by hand is a being just the same.
6
+ // What she acts through is `this.stance`.
7
+ import type { Ask, Asker, Blueprint, JsonObject, Reply, Schema, Stance } from './types.ts';
8
8
 
9
- // One declared ask. `for` decides whether this asker sees it, and so whether
10
- // this asker may call it: what she shows is what she can be asked.
9
+ // One declared ask. `for` decides whether this asker sees it, and so
10
+ // whether this asker may call it.
11
11
  export type AskSpec = {
12
- description?: string;
13
- input?: Schema;
14
- output?: Schema;
15
- for?: (occupant: OccupantRecord | undefined, asker: Asker) => boolean;
12
+ readonly description?: string;
13
+ readonly input?: Schema;
14
+ readonly for?: (asker: Asker, notes: JsonObject | undefined) => boolean;
16
15
  };
17
16
 
18
- // Names a subclass may not use for an ask, because they are the base's own.
19
- const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'standings', 'occupants', 'occupant', 'invite', 'knock', 'take', 'boot', 'lend', 'constructor']);
17
+ // Names a subclass may not declare as an ask, because they are the base's.
18
+ const RESERVED = new Set(['answer', 'describe', 'stance', 'cells', 'notes', 'constructor']);
20
19
 
21
- // Whether she has a method of that name, written on her own prototype chain
22
- // below Object's. A name Object lends every object, `hasOwnProperty` or
23
- // `toString`, is not a method she wrote; and a field she assigns in her own
24
- // constructor is not there yet when the base checks, so an ask is a method
25
- // on the prototype and nothing else.
20
+ // Whether she wrote a method of that name, below Object's prototype.
26
21
  const wrote = (self: object, name: string): boolean => {
27
22
  for (let p = Object.getPrototypeOf(self); p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
28
23
  if (Object.hasOwn(p, name)) return typeof (p as Record<string, unknown>)[name] === 'function';
@@ -30,18 +25,17 @@ const wrote = (self: object, name: string): boolean => {
30
25
  return false;
31
26
  };
32
27
 
33
- // Both statics below are read off the class the object was made from, so a
34
- // subclass declaring either replaces its parent's rather than adding to it.
35
- // That is the rule: her blueprint is exactly what the class in front of you
36
- // declares, in the order she chose, and merging down a chain would hand her
37
- // asks she may mean to drop and an order she did not write. A subclass that
38
- // means to extend says so, `static override asks = { ...Parent.asks, mine: {} }`.
28
+ type Method = (args: JsonObject, asker: Asker) => Reply | Promise<Reply>;
29
+
30
+ // A subclass's statics replace its parent's and never merge: her blueprint
31
+ // is what the class in front of you declares, in its order. Her kind is
32
+ // never inherited: see `kind.ts`.
39
33
  export class Being {
40
- // Her cells' defaults. Merged in at birth, only where a key is missing, so
41
- // a restart keeps what she wrote.
34
+ // The name her row keeps for this class. Every class that stands declares
35
+ // its own.
36
+ static readonly kind: string = 'org.nervur.being';
37
+ // Her cells' defaults, written at birth only where a key is missing.
42
38
  static cells: JsonObject = {};
43
- // What she can be asked. Declaration order is blueprint order, except a
44
- // name that reads as an array index, which the language lists first.
45
39
  static asks: Record<string, AskSpec> = {};
46
40
 
47
41
  readonly stance: Stance;
@@ -51,73 +45,36 @@ export class Being {
51
45
  const C = this.constructor as typeof Being;
52
46
  for (const name of Object.keys(C.asks)) {
53
47
  if (RESERVED.has(name)) throw new Error(`ask '${name}' is a reserved name`);
54
- if (!wrote(this, name)) throw new Error(`ask '${name}' has no method on the prototype`);
48
+ if (!wrote(this, name)) throw new Error(`ask '${name}' has no method`);
55
49
  }
56
- // Own keys only: a default named after a member of Object's prototype is
57
- // still hers, and still missing until she writes it.
58
50
  for (const [k, v] of Object.entries(C.cells)) if (!Object.hasOwn(stance.cells, k)) stance.cells[k] = structuredClone(v);
59
51
  }
60
52
 
61
- get cells(): Cells {
53
+ get cells(): JsonObject {
62
54
  return this.stance.cells;
63
55
  }
64
- get standings(): Standings {
65
- return this.stance.standings;
66
- }
67
- get occupants(): Occupants {
68
- return this.stance.occupants;
69
- }
70
- // A standing at one of the things this device can do, under an id of hers.
71
- // The ward knocks and takes it for her; the invitation never reaches her.
72
- lend(name: string, id: string): Promise<string | null> {
73
- return this.stance.lend(name, id);
74
- }
75
- invite(id: string, notes?: JsonObject): Promise<Invitation | null> {
76
- return this.stance.occupants.invite(id, notes);
77
- }
78
- knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer> {
79
- return this.stance.standings.knock(invitation, method, args, wanted);
80
- }
81
- take(id: string, invitation: Invitation): Promise<string | null> {
82
- return this.stance.standings.take(id, invitation);
83
- }
84
- // A new being of her ward, by class name, under a key she chooses. With an
85
- // id, she holds a standing to the being she made, who knows her by her key.
86
- boot(className: string, key: string, id?: string): Promise<string | null> {
87
- return this.stance.boot(className, key, id);
88
- }
89
- // The occupant record for whoever is at the door. Undefined at a public being.
90
- occupant(asker: Asker): OccupantRecord | undefined {
91
- return asker.id !== undefined && Object.hasOwn(this.cells.occupants, asker.id) ? this.cells.occupants[asker.id] : undefined;
56
+
57
+ // The notes she invited this asker under.
58
+ notes(asker: Asker): JsonObject | undefined {
59
+ return asker.id === undefined ? undefined : this.stance.occupants.notes(asker.id);
92
60
  }
93
61
 
94
- // Her blueprint for this asker. Override to shape it by hand.
95
62
  describe(asker: Asker): Blueprint {
96
63
  const C = this.constructor as typeof Being;
97
- const rec = this.occupant(asker);
64
+ const notes = this.notes(asker);
98
65
  const asks: Ask[] = [];
99
66
  for (const [name, spec] of Object.entries(C.asks)) {
100
- if (spec.for && !spec.for(rec, asker)) continue;
101
- const ask: Ask = { name, input: spec.input ?? { type: 'object' } };
102
- if (spec.description !== undefined) ask.description = spec.description;
103
- if (spec.output !== undefined) ask.output = spec.output;
104
- asks.push(ask);
67
+ if (spec.for && !spec.for(asker, notes)) continue;
68
+ asks.push({ name, ...(spec.description === undefined ? {} : { description: spec.description }), input: spec.input ?? { type: 'object' } });
105
69
  }
106
70
  return { asks, notes: {} };
107
71
  }
108
72
 
109
- // The one function. Override to wrap it; call super to keep the dispatch.
110
- async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
73
+ async answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply> {
111
74
  if (method === undefined) return this.describe(asker);
112
75
  const C = this.constructor as typeof Being;
113
- // Declared, by her, on purpose. `asks` is an ordinary object, so a bare
114
- // lookup would also find every name on Object's prototype: `valueOf`
115
- // would answer with her stance, `toString` with a string, and neither is
116
- // an ask she wrote. Only her own keys are asks, which is what describe
117
- // shows. What she shows is what she can be asked.
118
76
  const spec = Object.hasOwn(C.asks, method) ? C.asks[method] : undefined;
119
- if (!spec || (spec.for && !spec.for(this.occupant(asker), asker))) return { error: 'unknown ask' };
120
- const fn = (this as unknown as Record<string, (args: JsonObject, asker: Asker) => Reply | Promise<Reply>>)[method];
121
- return fn.call(this, args ?? {}, asker);
77
+ if (!spec || (spec.for && !spec.for(asker, this.notes(asker)))) return { error: 'unknown ask' };
78
+ return ((this as unknown as Record<string, Method>)[method] as Method).call(this, args, asker);
122
79
  }
123
80
  }
@@ -1,46 +1,18 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The digest: SHA-256, as hex, over the JCS (RFC 8785) canonical form of a
3
- // blueprint. Same bytes from every language. WebCrypto only, so it runs
4
- // wherever the language runs.
5
- import type { Json } from './types.ts';
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, type Json } from '../crypto/index.ts';
6
6
 
7
- // A value I-JSON has no room for: a key left empty, a function, a symbol.
8
- // None of them cross an edge, so none of them may reach a digest. A key
9
- // carrying one is dropped and a slot carrying one is null, which is what
10
- // crossing does to them, so the digest names what arrived and not what she
11
- // happened to be holding.
12
- const absent = (v: unknown): boolean => v === undefined || typeof v === 'function' || typeof v === 'symbol';
13
-
14
- // JCS for I-JSON values: sorted keys, no whitespace, JSON escaping. Numbers
15
- // are serialized as ES does, which is what RFC 8785 specifies. A hole in a
16
- // list is null, as JSON writes it.
17
7
  export const canonical = (v: Json): string => {
18
- if (Array.isArray(v)) return `[${Array.from(v, (slot) => (absent(slot) ? 'null' : canonical(slot))).join(',')}]`;
8
+ if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;
19
9
  if (v !== null && typeof v === 'object') {
20
10
  return `{${Object.keys(v)
21
- .filter((k) => !absent(v[k]))
22
11
  .sort()
23
- .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
12
+ .map((k) => `${JSON.stringify(k)}:${canonical(v[k]!)}`)
24
13
  .join(',')}}`;
25
14
  }
26
15
  return JSON.stringify(v);
27
16
  };
28
17
 
29
- // Hex and the guard are spelled here and again in `src/ward/arithmetic.ts`,
30
- // which is the price of the boundary: nothing under `src/being` imports
31
- // anything above it, because this is the whole world a being's own code sees
32
- // and a being reaching the ward is the thing the shape is against.
33
- const hex = (bytes: ArrayBuffer): string =>
34
- Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, '0')).join('');
35
-
36
- // `crypto.subtle` is read at the call and never captured at load. A browser
37
- // on a plain http:// origin has `crypto` without `subtle`, and a terrain may
38
- // install one after this module is first imported; either way the failure is
39
- // one sentence and not a TypeError from inside a digest nobody can read.
40
- const subtle = (): SubtleCrypto => {
41
- const s = globalThis.crypto?.subtle;
42
- if (!s) throw new Error('this terrain has no crypto.subtle: a being needs a secure context to be described');
43
- return s;
44
- };
45
-
46
- export const digest = async (blueprint: Json): Promise<string> => hex(await subtle().digest('SHA-256', new TextEncoder().encode(canonical(blueprint))));
18
+ export const digest = async (blueprint: Json): Promise<string> => hex(await sha256(utf8(canonical(blueprint))));
@@ -0,0 +1,73 @@
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
+ // static override kind = 'com.acme.timer' }
7
+ // class IntervalTimer extends Timer { one class that fulfils it
8
+ // static override kind = 'com.acme.interval-timer' }
9
+ //
10
+ // A being lends by the contract's kind and never learns the class. Every
11
+ // class between the faculty and `Faculty` is a contract and declares its
12
+ // own kind, and a faculty fulfils each of them and her own. Two asks are the
13
+ // faculty's own and answered to the ward-being that opened her alone:
14
+ // `offer`, an invitation on herself for a ward the dock vouches for, and
15
+ // `retract`, an offer that was not taken.
16
+ import { Being } from './being.ts';
17
+ import { kindOf } from './kind.ts';
18
+ import { WARD, type Asker, type Blueprint, type JsonObject, type Reply } from './types.ts';
19
+
20
+ export class Faculty extends Being {
21
+ static override readonly kind: string = 'org.nervur.faculty';
22
+
23
+ // The contracts a class fulfils: itself and every class between it and
24
+ // `Faculty`, by kind. A class among them with no kind of its own throws.
25
+ static contracts(C: abstract new (...args: never[]) => unknown): string[] {
26
+ const kinds: string[] = [];
27
+ for (let c: unknown = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c)) kinds.push(kindOf(c));
28
+ return kinds;
29
+ }
30
+
31
+ static fulfils(C: unknown): C is typeof Faculty {
32
+ return typeof C === 'function' && C.prototype instanceof Faculty;
33
+ }
34
+
35
+ #open(): Record<string, string> {
36
+ if (this.cells.offers === undefined) this.cells.offers = {};
37
+ return this.cells.offers as Record<string, string>;
38
+ }
39
+
40
+ async #offer(): Promise<JsonObject> {
41
+ const n = ((this.cells.offered as number | undefined) ?? 0) + 1;
42
+ this.cells.offered = n;
43
+ const id = `lent:${n}`;
44
+ const invitation = await this.stance.occupants.invite(id);
45
+ if (invitation === null) return { error: 'not invited' };
46
+ this.#open()[invitation.heir] = id;
47
+ return { invitation };
48
+ }
49
+
50
+ #retract(args: JsonObject): JsonObject {
51
+ const open = this.#open();
52
+ const id = typeof args.heir === 'string' && Object.hasOwn(open, args.heir) ? open[args.heir] : undefined;
53
+ if (id === undefined) return { retracted: null };
54
+ this.stance.occupants.remove(id);
55
+ delete open[args.heir as string];
56
+ return { retracted: id };
57
+ }
58
+
59
+ override describe(asker: Asker): Blueprint {
60
+ const blueprint = super.describe(asker);
61
+ if (asker.id !== WARD) return blueprint;
62
+ return { ...blueprint, asks: [...blueprint.asks, { name: 'offer', input: { type: 'object' } }, { name: 'retract', input: { type: 'object', required: ['heir'] } }] };
63
+ }
64
+
65
+ // An offer is closed by the first word from the occupant it was made for.
66
+ override answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply> {
67
+ if (asker.id === WARD && method === 'offer') return this.#offer();
68
+ if (asker.id === WARD && method === 'retract') return Promise.resolve(this.#retract(args));
69
+ const open = this.#open();
70
+ for (const [heir, id] of Object.entries(open)) if (id === asker.id) delete open[heir];
71
+ return super.answer(asker, method, args);
72
+ }
73
+ }
@@ -1,8 +1,8 @@
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, type AskSpec } from './being.ts';
4
- export { Lent, BOX, LENT_ASKS } from './lent.ts';
5
- export { silence, isSilence, answered, unreached, isUnreached, word, isWord, wordOf, told, DOOR_WORDS, isDoorWord } from './silence.ts';
6
- export { digest, canonical } from './digest.ts';
7
- export { OWNER, PUBLIC, RESERVED_IDS, isBlueprint, invitationArgs, isInvitation } from './types.ts';
8
- export type * from './types.ts';
4
+ export { canonical, digest } from './digest.ts';
5
+ export { Faculty } from './faculty.ts';
6
+ export { isKind, KIT, kindOf, ownKind } from './kind.ts';
7
+ 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';
8
+ export { isSilence, isWord, silence, told, word, type Silence, type Word, type WordName } from './words.ts';
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A kind: the name a row keeps for the class a being is born of, and the
3
+ // name a contract is lent by. It is declared on the class, never taken from
4
+ // the name the language gives it, because a bundler renames classes.
5
+ //
6
+ // static kind = 'com.acme.shop'
7
+ //
8
+ // A kind is a reversed domain its author owns, then a name: at least three
9
+ // segments joined by `.`, each lowercase letters, digits and `-`, starting
10
+ // with a letter or digit. `org.nervur.` is the kit's own. A class declares
11
+ // its own kind: one it inherits names its parent, so it is none.
12
+ export const KIT = 'org.nervur.';
13
+
14
+ const KIND = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*){2,}$/;
15
+
16
+ export const isKind = (text: unknown): text is string => typeof text === 'string' && KIND.test(text);
17
+
18
+ // The kind a class declares on itself, or undefined.
19
+ export const ownKind = (C: unknown): string | undefined => {
20
+ if (typeof C !== 'function' || !Object.hasOwn(C, 'kind')) return undefined;
21
+ const kind = (C as { kind?: unknown }).kind;
22
+ return isKind(kind) ? kind : undefined;
23
+ };
24
+
25
+ // The kind a class declares on itself, or a throw naming the class.
26
+ export const kindOf = (C: unknown): string => {
27
+ const kind = ownKind(C);
28
+ if (kind === undefined) throw new Error(`class ${typeof C === 'function' ? C.name : String(C)} declares no kind of its own`);
29
+ return kind;
30
+ };