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/package.json CHANGED
@@ -1,54 +1,40 @@
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.0",
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"
20
- },
21
15
  "exports": {
22
16
  ".": {
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"
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
37
19
  },
38
- "./vector": {
39
- "types": "./dist/vector/index.d.ts",
40
- "default": "./dist/vector/index.js"
41
- },
42
- "./package.json": "./package.json"
20
+ "./folder": {
21
+ "types": "./dist/folder/index.d.ts",
22
+ "default": "./dist/folder/index.js"
23
+ }
43
24
  },
44
25
  "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\"",
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "check": "tsc -p tsconfig.json && npm run build && node ../../test.mjs \"test/unit/*.test.ts\" \"test/e2e/*.test.ts\"",
28
+ "deep": "node ../../test.mjs \"test/terrain/*.test.ts\"",
29
+ "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
30
  "prepublishOnly": "test \"$NERVUR_GATED\" = 1 || { echo 'a publish is /release, from the root, on the human'\"'\"'s word' >&2; exit 1; }"
51
31
  },
32
+ "engines": {
33
+ "node": ">=22.18"
34
+ },
35
+ "dependencies": {
36
+ "@noble/post-quantum": "0.7.1"
37
+ },
52
38
  "publishConfig": {
53
39
  "access": "public"
54
40
  },
@@ -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,13 @@ 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.
39
32
  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.
33
+ // Her cells' defaults, written at birth only where a key is missing.
42
34
  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
35
  static asks: Record<string, AskSpec> = {};
46
36
 
47
37
  readonly stance: Stance;
@@ -51,73 +41,36 @@ export class Being {
51
41
  const C = this.constructor as typeof Being;
52
42
  for (const name of Object.keys(C.asks)) {
53
43
  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`);
44
+ if (!wrote(this, name)) throw new Error(`ask '${name}' has no method`);
55
45
  }
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
46
  for (const [k, v] of Object.entries(C.cells)) if (!Object.hasOwn(stance.cells, k)) stance.cells[k] = structuredClone(v);
59
47
  }
60
48
 
61
- get cells(): Cells {
49
+ get cells(): JsonObject {
62
50
  return this.stance.cells;
63
51
  }
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;
52
+
53
+ // The notes she invited this asker under.
54
+ notes(asker: Asker): JsonObject | undefined {
55
+ return asker.id === undefined ? undefined : this.stance.occupants.notes(asker.id);
92
56
  }
93
57
 
94
- // Her blueprint for this asker. Override to shape it by hand.
95
58
  describe(asker: Asker): Blueprint {
96
59
  const C = this.constructor as typeof Being;
97
- const rec = this.occupant(asker);
60
+ const notes = this.notes(asker);
98
61
  const asks: Ask[] = [];
99
62
  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);
63
+ if (spec.for && !spec.for(asker, notes)) continue;
64
+ asks.push({ name, ...(spec.description === undefined ? {} : { description: spec.description }), input: spec.input ?? { type: 'object' } });
105
65
  }
106
66
  return { asks, notes: {} };
107
67
  }
108
68
 
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> {
69
+ async answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply> {
111
70
  if (method === undefined) return this.describe(asker);
112
71
  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
72
  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);
73
+ if (!spec || (spec.for && !spec.for(asker, this.notes(asker)))) return { error: 'unknown ask' };
74
+ return ((this as unknown as Record<string, Method>)[method] as Method).call(this, args, asker);
122
75
  }
123
76
  }
@@ -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,66 @@
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.ts';
13
+ import { WARD, type Asker, type Blueprint, type JsonObject, type Reply } from './types.ts';
14
+
15
+ export class Faculty extends Being {
16
+ // The contracts a class fulfils: itself and every class between it and
17
+ // `Faculty`, by name.
18
+ static contracts(C: abstract new (...args: never[]) => unknown): string[] {
19
+ const names: string[] = [];
20
+ for (let c: unknown = C; typeof c === 'function' && c !== Faculty; c = Object.getPrototypeOf(c)) names.push((c as { name: string }).name);
21
+ return names;
22
+ }
23
+
24
+ static fulfils(C: unknown): C is typeof Faculty {
25
+ return typeof C === 'function' && C.prototype instanceof Faculty;
26
+ }
27
+
28
+ #open(): Record<string, string> {
29
+ if (this.cells.offers === undefined) this.cells.offers = {};
30
+ return this.cells.offers as Record<string, string>;
31
+ }
32
+
33
+ async #offer(): Promise<JsonObject> {
34
+ const n = ((this.cells.offered as number | undefined) ?? 0) + 1;
35
+ this.cells.offered = n;
36
+ const id = `lent:${n}`;
37
+ const invitation = await this.stance.occupants.invite(id);
38
+ if (invitation === null) return { error: 'not invited' };
39
+ this.#open()[invitation.heir] = id;
40
+ return { invitation };
41
+ }
42
+
43
+ #retract(args: JsonObject): JsonObject {
44
+ const open = this.#open();
45
+ const id = typeof args.heir === 'string' && Object.hasOwn(open, args.heir) ? open[args.heir] : undefined;
46
+ if (id === undefined) return { retracted: null };
47
+ this.stance.occupants.remove(id);
48
+ delete open[args.heir as string];
49
+ return { retracted: id };
50
+ }
51
+
52
+ override describe(asker: Asker): Blueprint {
53
+ const blueprint = super.describe(asker);
54
+ if (asker.id !== WARD) return blueprint;
55
+ return { ...blueprint, asks: [...blueprint.asks, { name: 'offer', input: { type: 'object' } }, { name: 'retract', input: { type: 'object', required: ['heir'] } }] };
56
+ }
57
+
58
+ // An offer is closed by the first word from the occupant it was made for.
59
+ override answer(asker: Asker, method: string | undefined, args: JsonObject): Promise<Reply> {
60
+ if (asker.id === WARD && method === 'offer') return this.#offer();
61
+ if (asker.id === WARD && method === 'retract') return Promise.resolve(this.#retract(args));
62
+ const open = this.#open();
63
+ for (const [heir, id] of Object.entries(open)) if (id === asker.id) delete open[heir];
64
+ return super.answer(asker, method, args);
65
+ }
66
+ }
@@ -1,8 +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, 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 { 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';
7
+ export { isSilence, isWord, silence, told, word, type Silence, type Word, type WordName } from './words.ts';
@@ -1,170 +1,72 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The Being side of Quo, as types. Everything a being author can hold.
3
- // Nothing here knows a runtime, a key, or a wire.
4
- import type { silence } from './silence.ts';
2
+ // Everything a being author holds, as types.
3
+ import type { Json } from '../crypto/index.ts';
4
+ import type { Invitation } from '../quo/index.ts';
5
+ import type { Silence, Word } from './words.ts';
5
6
 
6
- // I-JSON. Everything that crosses an edge.
7
- export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
7
+ export type { Invitation, Json };
8
8
  export type JsonObject = { [key: string]: Json };
9
9
 
10
- // The asker: her own id for the being at the door. {} at a public being.
11
- export type Asker = { id: string } | { id?: undefined };
12
-
13
- // The ids the ward speaks under, and no being may mint. An asker id is hers:
14
- // she chooses the strings, and the ward has no business in her namespace. But
15
- // the ward must sometimes stand at her door itself — its describe asks every
16
- // being under OWNER — and an id it shares with one of her occupants is two
17
- // different parties wearing one name. She could not tell them apart, and the
18
- // ward would shape its describe through an occupant's gate.
19
- //
20
- // So the ward's words are shouted, and refused at the mint. A word can only
21
- // be reserved before anyone has used it: a partition already holding an
22
- // occupant of that name could never have it taken back. PUBLIC guards nothing
23
- // today — a public asker is {} and carries no id at all — and that is exactly
24
- // why it is claimed now, while claiming it is free.
10
+ // Who is at the door: the id she filed the occupant under, `{}` for nobody,
11
+ // and `{ id: OWNER }` for whoever holds the ward's unsealed ask.
12
+ export type Asker = { readonly id: string } | { readonly id?: undefined };
25
13
  export const OWNER = 'OWNER';
26
- export const PUBLIC = 'PUBLIC';
27
- export const RESERVED_IDS: readonly string[] = [OWNER, PUBLIC];
28
14
 
29
- // An invitation is a value. Opaque to her: the far ward's pk, a heir pk,
30
- // and the heir's secret. Without a heir it addresses a ward's public being.
31
- export type Invitation = { ward: string; heir?: string; secret?: string };
15
+ // The ward-being's key in her ward, and so the id every being she makes
16
+ // knows her by.
17
+ export const WARD = 'ward';
32
18
 
33
- // An invitation as one object of values, which is what it already is. A being
34
- // who hands one on sends it as args, and args are values; this is the one
35
- // place that is spelled, so nobody spells it with a cast. Absent stays absent:
36
- // an invitation to a public being carries no heir and no secret, and a key
37
- // present and undefined is not the same object once JSON has been through it.
38
- export const invitationArgs = (inv: Invitation): JsonObject => ({
39
- ward: inv.ward,
40
- ...(inv.heir !== undefined ? { heir: inv.heir } : {}),
41
- ...(inv.secret !== undefined ? { secret: inv.secret } : {}),
42
- });
19
+ // What comes back from an ask, and what a being answers.
20
+ export type Answer = Json | Silence | Word;
21
+ export type Reply = Json | Silence | undefined;
43
22
 
44
- // Whether a value that arrived is an invitation. The shape is the spec's, so
45
- // the reading of it is too, and it is one reading: a form that
46
- // asks a guest for one, a lent being handed one to wake her maker by, and a
47
- // shell that finds one in a link all ask the same question, and a kinder
48
- // answer in one of them is a value that fails at a door instead of at the
49
- // edge it came in by. A ward pk is a hundred and twenty-eight lowercase hex,
50
- // and a heir comes with its secret or neither comes: an invitation to a
51
- // public being carries no heir, and a heir with no secret opens nothing.
52
- export const isInvitation = (v: unknown): v is Invitation => {
53
- if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
54
- const o = v as Record<string, unknown>;
55
- if (typeof o.ward !== 'string' || !/^[0-9a-f]{128}$/.test(o.ward)) return false;
56
- const heir = typeof o.heir === 'string',
57
- secret = typeof o.secret === 'string';
58
- return (heir && secret) || (!heir && !secret && !('heir' in o) && !('secret' in o));
59
- };
23
+ export type Wanted = { readonly time?: number };
60
24
 
61
- // A blueprint is an MCP tool list plus notes.
25
+ // A blueprint: the asks a being can be asked, as an MCP tool list, and notes.
62
26
  export type Schema = JsonObject;
63
- export type Ask = { name: string; description?: string; input: Schema; output?: Schema };
27
+ export type Ask = { name: string; description?: string; input: Schema };
64
28
  export type Blueprint = { asks: Ask[]; notes: Json };
65
29
 
66
- // Whether what came back from an empty ask is a blueprint. Every describe on
67
- // the far side of a door is somebody else's code, so nothing may be written
68
- // into a standing's record as a blueprint without being read as one first: a
69
- // side walks `asks` by name, and a list of anything else, or no list at all,
70
- // is a side that breaks on a far ward's answer. The asks are checked to the
71
- // depth a side reads them, a name and an input each, and no further: what a
72
- // far being puts beside those is hers.
73
- export const isBlueprint = (v: unknown): v is Blueprint => {
74
- if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
75
- const { asks, notes } = v as { asks?: unknown; notes?: unknown };
76
- if (!Array.isArray(asks) || notes === undefined) return false;
77
- return asks.every((a) => a !== null && typeof a === 'object' && !Array.isArray(a) && typeof (a as Ask).name === 'string' && (a as Ask).input !== null && typeof (a as Ask).input === 'object' && !Array.isArray((a as Ask).input));
78
- };
79
-
80
- export type StandingRecord = { id: string; digest: string | null; blueprint: Blueprint | null; seen: string | null };
81
- export type OccupantRecord = { id: string; notes: JsonObject };
82
- export type Cells = {
83
- standings: Record<string, StandingRecord>;
84
- occupants: Record<string, OccupantRecord>;
85
- [hers: string]: Json;
86
- };
87
-
88
- export type Silence = typeof silence;
89
-
90
- // The ward's words: what her ward says when no object came back and it knows
91
- // why. A door says the first five to a key it has bound, and rides them on
92
- // the wire as `{ quo: word }`; her own ward says the last four to her and
93
- // they never leave the ward. Each is one frozen object under one symbol key,
94
- // so no being makes one by accident and none crosses an edge as a value. A
95
- // being who reaches for the symbol on purpose makes an object her ward reads
96
- // as her having thrown, which is what a being who returns nonsense gets: the
97
- // key is a guard against collision, never against her.
98
- export type DoorWord = 'removed' | 'absent' | 'unannounced' | 'repeated' | 'threw';
99
- export type WardWord = 'unreached' | 'late' | 'invitation' | 'dropped';
100
- export type WordName = DoorWord | WardWord;
101
- export const WORD_KEY: unique symbol = Symbol.for('quo.word');
102
- export type Word<W extends WordName = WordName> = { readonly [K in typeof WORD_KEY]: W };
103
- export type Unreached = Word<'unreached'>;
104
-
105
- // What comes back from an ask or a knock.
106
- export type Answer = Json | Silence | Word;
107
- // What a being answers.
108
- export type Reply = Json | Silence;
109
-
110
- // What a being may ask for when she wants to say so herself: the time this
111
- // one ask may spend. Optional, and so is saying anything at all: an ask that
112
- // says nothing gets her ward's default, which is the ordinary way to ask.
113
- export type Wanted = { time?: number };
114
-
115
- export type Standing = {
30
+ export interface Standing {
116
31
  readonly id: string;
117
32
  ask(method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
118
- };
33
+ }
119
34
 
120
- export type Standings = {
121
- knock(invitation: Invitation, method?: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
35
+ // Her side of the relations she holds.
36
+ export interface Standings {
37
+ // Knocks with the empty ask and holds the relation under `id`: the id, or
38
+ // null where the id is taken or reserved, or no object came back.
122
39
  take(id: string, invitation: Invitation): Promise<string | null>;
123
- remove(id: string): void;
124
- } & { readonly [id: string]: Standing | undefined };
40
+ get(id: string): Standing | undefined;
41
+ ids(): string[];
42
+ remove(id: string): boolean;
43
+ }
125
44
 
126
- export type Occupants = {
127
- // awaitable: a key is minted. The notes are the terms it is minted under,
128
- // written on the occupant for her gate to read, and hers alone after that.
45
+ // The door's side: the ids she invited.
46
+ export interface Occupants {
129
47
  invite(id: string, notes?: JsonObject): Promise<Invitation | null>;
48
+ notes(id: string): JsonObject | undefined;
49
+ ids(): string[];
50
+ remove(id: string): boolean;
51
+ }
130
52
 
131
- remove(id: string): void;
132
- };
133
-
134
- // The stance. What every being, in every language, is handed at birth.
135
- export type Stance = {
136
- readonly cells: Cells;
53
+ // What every being is handed at birth.
54
+ export interface Stance {
55
+ readonly key: string;
56
+ readonly cells: JsonObject;
137
57
  readonly occupants: Occupants;
138
58
  readonly standings: Standings;
139
- // A standing at one of the things this device can do, by the harbor's name
140
- // for it, under an id of hers. The id back, or null: this box lends no such
141
- // name, the id already names a record or is a reserved word, or the
142
- // relation was refused.
143
- //
144
- // Her ward asks the ground, then knocks and takes in her name, as it does
145
- // for a being she boots. So the invitation never reaches her, because it is
146
- // the device's capability and not her own relation to give away; what she
147
- // holds afterwards is an ordinary standing that counts, rotates, can be
148
- // removed and hears `removed`.
149
- //
150
- // The ground is the booting harbor's, so this reaches the box she is
151
- // running on and no other. A standing she wakes up holding names a being on
152
- // the box she was on when it was made, and a migration is a restart she
153
- // cannot tell from any other, so a being who wants the box she is on asks
154
- // again at every birth.
155
- lend(name: string, id: string): Promise<string | null>;
156
- // A new being of her ward, by class name, under a key she chooses. The key
157
- // back, or null: the key is taken, the harbor holds no such class, or the
158
- // class threw at birth. A being may make; only the owner reaches into
159
- // another. Naming an id asks for a way back too: the being made holds an
160
- // occupant for her maker under the maker's own key, and the maker holds
161
- // the standing under that id. A relation that could not be made is a boot
162
- // that made nobody, and the being made goes with it.
59
+ // A new being of her ward, by class name, under a key she chooses. With an
60
+ // id, the new being invites her under her key, and she takes it under id.
163
61
  boot(className: string, key: string, id?: string): Promise<string | null>;
164
- };
62
+ // A standing under `id` at the faculty this harbor opened for a contract:
63
+ // the id, or null where none is open or the relation was refused.
64
+ lend(contract: { readonly name: string }, id: string): Promise<string | null>;
65
+ }
165
66
 
166
- // The raw shape of a being. Anything with these two is a being.
67
+ // Anything with `answer` is a being.
167
68
  export interface BeingLike {
168
- answer(asker: Asker, method?: string, args?: JsonObject): Reply | Promise<Reply>;
69
+ answer(asker: Asker, method: string | undefined, args: JsonObject): Reply | Promise<Reply>;
70
+ describe?(asker: Asker): Blueprint;
169
71
  }
170
- export type BeingClass = new (stance: Stance) => BeingLike;
72
+ export type BeingClass = (new (stance: Stance) => BeingLike) & { readonly name: string };
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // What a being is told when no object came back. Silence is one symbol.
3
+ // A word is one frozen object per name, so a being never makes one by
4
+ // accident, and two copies of the package in one bundle still agree.
5
+ //
6
+ // removed, unannounced, repeated a far door's words, as Quo says them
7
+ // unreached nothing came back: asking again is safe
8
+ // late the allowance ran out
9
+ // dropped the standing was removed while asking
10
+ // invitation what was handed is no invitation
11
+ import type { Word as DoorWord } from '../quo/index.ts';
12
+
13
+ export const silence: unique symbol = Symbol.for('nervur.silence');
14
+ export type Silence = typeof silence;
15
+ export const isSilence = (x: unknown): x is Silence => x === silence;
16
+
17
+ export type WordName = DoorWord | 'unreached' | 'late' | 'dropped' | 'invitation';
18
+ const KEY: unique symbol = Symbol.for('nervur.word');
19
+ export type Word<W extends WordName = WordName> = { readonly [KEY]: W };
20
+
21
+ const made = new Map<WordName, Word>();
22
+ export const word = <W extends WordName>(name: W): Word<W> => {
23
+ let w = made.get(name);
24
+ if (!w) made.set(name, (w = Object.freeze({ [KEY]: name })));
25
+ return w as Word<W>;
26
+ };
27
+ export const isWord = (x: unknown): x is Word => typeof x === 'object' && x !== null && typeof (x as Record<symbol, unknown>)[KEY] === 'string';
28
+
29
+ // A word by its name, and anything else as it is, so one comparison says
30
+ // which answer came back.
31
+ export const told = (x: unknown): unknown => (isWord(x) ? x[KEY] : x);