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
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The root line: one request, one answer, both plain JSON, for whatever
3
+ // carries the root's asks to a harbor in its own process. The command line
4
+ // carries it over a local socket, and an edge route carries it the same way.
5
+ //
6
+ // request { ward?, method?, args? } no ward is the box ward, no
7
+ // method the census
8
+ // answer { answer } what the ward-being said
9
+ // { silence: true } she said nothing
10
+ // { word } a kit word: unreached, late, ...
11
+ // { error } the request was not one, or the
12
+ // ward is none this harbor holds
13
+ import { isSilence, isWord, told, type Json, type JsonObject } from '../being/index.ts';
14
+ import type { Harbor } from './harbor.ts';
15
+
16
+ export type RootRequest = { ward?: string; method?: string; args?: JsonObject };
17
+
18
+ const object = (v: unknown): v is JsonObject => typeof v === 'object' && v !== null && !Array.isArray(v);
19
+
20
+ export const readRootRequest = (value: unknown): RootRequest | null => {
21
+ if (!object(value)) return null;
22
+ const { ward, method, args } = value;
23
+ if ((ward !== undefined && typeof ward !== 'string') || (method !== undefined && typeof method !== 'string') || (args !== undefined && !object(args))) return null;
24
+ return { ...(ward === undefined ? {} : { ward }), ...(method === undefined ? {} : { method }), ...(args === undefined ? {} : { args }) };
25
+ };
26
+
27
+ export const rootLine = async (harbor: Harbor, value: unknown): Promise<JsonObject> => {
28
+ const request = readRootRequest(value);
29
+ if (request === null) return { error: 'not a root request' };
30
+ const ward = request.ward === undefined ? harbor.box : harbor.ward(request.ward);
31
+ if (ward === undefined) return { error: 'no such ward' };
32
+ const out = await ward.root(request.method, request.args);
33
+ if (isSilence(out)) return { silence: true };
34
+ if (isWord(out)) return { word: told(out) as string };
35
+ return { answer: out as Json };
36
+ };
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Where a harbor runs. A terrain hands the harbor one body for each
3
+ // contract the onion cannot unpack from cells.
4
+ import type { Carrier, Clock, Custody, Entropy, Loader, Memory } from '../contract/index.ts';
5
+
6
+ export abstract class Terrain {
7
+ abstract readonly entropy: Entropy;
8
+ abstract readonly clock: Clock;
9
+ abstract readonly custody: Custody;
10
+ abstract readonly memory: Memory;
11
+ // The code the catalogue's cells name.
12
+ abstract readonly loader: Loader;
13
+ // What reaches a ward pk this harbor does not hold.
14
+ abstract readonly carrier: Carrier;
15
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `nervur`: what a stranger writes a world with. It imports no platform, so
3
+ // it runs wherever JavaScript runs. A disk is `nervur/folder`.
4
+
5
+ // Beings, faculties, and what they are told.
6
+ export { Being, canonical, digest, Faculty, isSilence, isWord, OWNER, silence, told, WARD, word } from './being/index.ts';
7
+ export type { Answer, Ask, Asker, AskSpec, BeingClass, BeingLike, Blueprint, Invitation, Json, JsonObject, Occupants, Reply, Schema, Silence, Stance, Standing, Standings, Wanted, Word, WordName } from './being/index.ts';
8
+ export { readInvitation } from './quo/index.ts';
9
+
10
+ // What a terrain gives a harbor.
11
+ export { Carrier, Clock, Custody, Entropy, Loader, Memory, type Loaded, type Module, type Rows } from './contract/index.ts';
12
+
13
+ // Wards and the ward-being.
14
+ export { CEILING, DEFAULT, DEPTH, ownerAsk, Pilot, pilots, WardBeing, type Steward, type WardBeingStance } from './ward/index.ts';
15
+
16
+ // The onion, and the one line its root's asks cross from any front.
17
+ export { BOX, Catalogue, Dock, Harbor, readRootRequest, Registry, rootLine, Terrain, type RootRequest } from './harbor/index.ts';
18
+
19
+ // The pointer dock: a whole world in one process.
20
+ export { CryptoEntropy, HeldCustody, HeldLoader, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World, type PointerParts } from './pointer/index.ts';
@@ -0,0 +1,65 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The pointer terrain's bodies: each contract fulfilled in one process,
3
+ // with nothing beyond JavaScript.
4
+ import { Clock, Custody, Entropy, Loader, Memory, type Loaded, type Module } from '../contract/index.ts';
5
+
6
+ export class CryptoEntropy extends Entropy {
7
+ draw(length: number): Uint8Array {
8
+ return globalThis.crypto.getRandomValues(new Uint8Array(length));
9
+ }
10
+ }
11
+
12
+ export class SystemClock extends Clock {
13
+ now(): number {
14
+ return Date.now();
15
+ }
16
+ wait(ms: number): { done: Promise<void>; cancel(): void } {
17
+ let timer: ReturnType<typeof setTimeout> | undefined;
18
+ const done = new Promise<void>((resolve) => (timer = setTimeout(resolve, ms)));
19
+ return { done, cancel: () => clearTimeout(timer) };
20
+ }
21
+ }
22
+
23
+ // The harbor's seed, drawn once and held by this object.
24
+ export class HeldCustody extends Custody {
25
+ readonly #seed: Uint8Array;
26
+ constructor(entropy: Entropy) {
27
+ super();
28
+ this.#seed = entropy.draw(32);
29
+ }
30
+ seed(): Promise<Uint8Array> {
31
+ return Promise.resolve(this.#seed.slice());
32
+ }
33
+ }
34
+
35
+ // Modules handed to this object, found by the name each declares.
36
+ export class HeldLoader extends Loader {
37
+ readonly #modules = new Map<string, Module>();
38
+ constructor(modules: readonly Module[] = []) {
39
+ super();
40
+ for (const m of modules) this.#modules.set(m.module, m);
41
+ }
42
+ load(from: string): Promise<Loaded | null> {
43
+ const m = this.#modules.get(from);
44
+ return Promise.resolve(m ? { module: m.module, version: m.version, classes: m.classes, from } : null);
45
+ }
46
+ }
47
+
48
+ // Entries that live as long as this object, copied in and out, which is
49
+ // what a restart in one process finds.
50
+ export class VolatileMemory extends Memory {
51
+ readonly #places = new Map<string, Map<string, Uint8Array>>();
52
+ read(place: string): Promise<Map<string, Uint8Array>> {
53
+ const entries = this.#places.get(place) ?? new Map<string, Uint8Array>();
54
+ return Promise.resolve(new Map([...entries].map(([name, bytes]) => [name, bytes.slice()])));
55
+ }
56
+ write(place: string, entries: ReadonlyMap<string, Uint8Array | null>): Promise<void> {
57
+ let kept = this.#places.get(place);
58
+ if (!kept) this.#places.set(place, (kept = new Map()));
59
+ for (const [name, bytes] of entries) {
60
+ if (bytes === null) kept.delete(name);
61
+ else kept.set(name, bytes.slice());
62
+ }
63
+ return Promise.resolve();
64
+ }
65
+ }
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The pointer dock: a whole world with `nervur` alone.
3
+ export { CryptoEntropy, HeldCustody, HeldLoader, SystemClock, VolatileMemory } from './bodies.ts';
4
+ export { NoCarrier, PointerCarrier, PointerTerrain, World, type PointerParts } from './world.ts';
@@ -0,0 +1,94 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The pointer world: harbors in one process, an address book from ward pk
3
+ // to harbor that stands in for the internet, and weather between them. A
4
+ // restart drops a harbor and opens a new one on the terrain the world
5
+ // keeps for it, so only what its memory kept comes back.
6
+ import { Carrier, type Clock, type Custody, type Entropy, type Loader, type Memory, type Module } from '../contract/index.ts';
7
+ import { Harbor, Terrain } from '../harbor/index.ts';
8
+ import { CryptoEntropy, HeldCustody, HeldLoader, SystemClock, VolatileMemory } from './bodies.ts';
9
+
10
+ // A carrier that reaches no one: a harbor alone.
11
+ export class NoCarrier extends Carrier {
12
+ carry(): Promise<Uint8Array | null> {
13
+ return Promise.resolve(null);
14
+ }
15
+ }
16
+
17
+ // Bytes to whichever harbor of a world holds the ward pk.
18
+ export class PointerCarrier extends Carrier {
19
+ readonly #world: World;
20
+ constructor(world: World) {
21
+ super();
22
+ this.#world = world;
23
+ }
24
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
25
+ return this.#world.carry(pk, bytes);
26
+ }
27
+ }
28
+
29
+ export type PointerParts = { entropy?: Entropy; clock?: Clock; custody?: Custody; memory?: Memory; carrier?: Carrier; loader?: Loader };
30
+
31
+ export class PointerTerrain extends Terrain {
32
+ readonly entropy: Entropy;
33
+ readonly clock: Clock;
34
+ readonly custody: Custody;
35
+ readonly memory: Memory;
36
+ readonly carrier: Carrier;
37
+ readonly loader: Loader;
38
+
39
+ constructor(parts: PointerParts = {}) {
40
+ super();
41
+ this.entropy = parts.entropy ?? new CryptoEntropy();
42
+ this.clock = parts.clock ?? new SystemClock();
43
+ this.custody = parts.custody ?? new HeldCustody(this.entropy);
44
+ this.memory = parts.memory ?? new VolatileMemory();
45
+ this.carrier = parts.carrier ?? new NoCarrier();
46
+ this.loader = parts.loader ?? new HeldLoader();
47
+ }
48
+ }
49
+
50
+ type Place = { harbor: Harbor; terrain: Terrain };
51
+
52
+ export class World {
53
+ readonly #places = new Map<string, Place>();
54
+ // Harbors the world reaches no one in, nor anyone out of.
55
+ readonly cut = new Set<string>();
56
+
57
+ // A harbor of this world, whose loader holds `modules` unless `parts`
58
+ // names a loader. `parts` replaces the pointer bodies, a folder's memory
59
+ // and custody among them. The harbor runs only the modules its catalogue
60
+ // was told to add.
61
+ async harbor(name: string, modules: readonly Module[] = [], parts: Omit<PointerParts, 'carrier'> = {}): Promise<Harbor> {
62
+ if (this.#places.has(name)) throw new Error(`a harbor named ${name} stands`);
63
+ const terrain = new PointerTerrain({ loader: new HeldLoader(modules), ...parts, carrier: new PointerCarrier(this) });
64
+ const harbor = await Harbor.open(terrain);
65
+ this.#places.set(name, { harbor, terrain });
66
+ return harbor;
67
+ }
68
+
69
+ get(name: string): Harbor {
70
+ const place = this.#places.get(name);
71
+ if (!place) throw new Error(`no harbor named ${name}`);
72
+ return place.harbor;
73
+ }
74
+
75
+ // The harbor opened again on its own terrain. `parts` replaces bodies of
76
+ // the terrain, as a new process would build them over the same disk, or
77
+ // with other code.
78
+ async restart(name: string, parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor> {
79
+ const place = this.#places.get(name);
80
+ if (!place) throw new Error(`no harbor named ${name}`);
81
+ const t = place.terrain;
82
+ const terrain = parts ? new PointerTerrain({ entropy: t.entropy, clock: t.clock, custody: t.custody, memory: t.memory, loader: t.loader, ...parts, carrier: t.carrier }) : t;
83
+ const harbor = await Harbor.open(terrain);
84
+ this.#places.set(name, { harbor, terrain });
85
+ return harbor;
86
+ }
87
+
88
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
89
+ for (const [name, { harbor }] of this.#places) {
90
+ if (harbor.holds(pk)) return this.cut.has(name) ? Promise.resolve(null) : harbor.arrive(pk, bytes);
91
+ }
92
+ return Promise.resolve(null);
93
+ }
94
+ }
@@ -0,0 +1,178 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The door: an ask's box in, a reply's box out, judged by the thirteen cases
3
+ // of the spec in their order. A stranger hears one silence whatever the
4
+ // case. The case is handed back beside the bytes, for the kit's own eyes,
5
+ // with `heard`: whether a key the door holds or keeps spoke.
6
+ import { decapsulate, hex, unhex, utf8, x25519Public, zero } from '../crypto/index.ts';
7
+ import { drawLock, SigningKey, type Draw, type WardKey } from './keys.ts';
8
+ import type { Invitation } from './invitation.ts';
9
+ import { readPayload, type Payload } from './payload.ts';
10
+ import type { HeirRecord, Keys, Relations } from './relations.ts';
11
+ import { writeReply, type Reply } from './reply.ts';
12
+ import { checkBody, follow, HEAD, knockEdge, openBody, openHead, replyKey, sealReply, signBody, SIZE, splitBody, splitKnock, ZERO_EDGE, type OpenedHead, type ReplyKey } from './seal.ts';
13
+
14
+ // The count numbers below the highest a door still honours: those within
15
+ // this span that it has not seen.
16
+ export const SPAN = 64;
17
+
18
+ export type Choice = { readonly object: string; readonly seen: string | null } | { readonly silence: true };
19
+
20
+ // What stands behind the door. `zero` says whether anything answers the
21
+ // zero head.
22
+ export interface Behind {
23
+ readonly zero: boolean;
24
+ answer(heir: string | null, payload: Payload): Promise<Choice>;
25
+ }
26
+
27
+ export type Judged = { bytes: Uint8Array; case: number; heard: boolean };
28
+
29
+ type Body = { edge: Uint8Array; text: Uint8Array; signature: Uint8Array };
30
+
31
+ export class Door {
32
+ readonly key: WardKey;
33
+ readonly relations: Relations;
34
+ readonly behind: Behind;
35
+ readonly draw: Draw;
36
+
37
+ constructor(key: WardKey, relations: Relations, behind: Behind, draw: Draw) {
38
+ this.key = key;
39
+ this.relations = relations;
40
+ this.behind = behind;
41
+ this.draw = draw;
42
+ }
43
+
44
+ // A new heir, held fresh, and its invitation. The lock is drawn at the
45
+ // first invitation that names an heir, before the heir's secret.
46
+ async invite(): Promise<{ heir: string; invitation: Invitation }> {
47
+ let lock = this.relations.lock();
48
+ if (!lock) this.relations.setLock((lock = drawLock(this.draw)));
49
+ const heir = await SigningKey.draw(this.draw);
50
+ this.relations.set(heir.id, { state: 'fresh' });
51
+ return { heir: heir.id, invitation: { ward: this.key.pk, heir: heir.id, secret: hex(heir.secret), lock: hex(lock.ek) } };
52
+ }
53
+
54
+ // The door stops holding an heir. A fresh one leaves nothing, a spent one
55
+ // leaves the keys kept at removal.
56
+ remove(heir: string): boolean {
57
+ const record = this.relations.get(heir);
58
+ if (record === undefined || record.state === 'kept') return false;
59
+ if (record.state === 'fresh') this.relations.delete(heir);
60
+ else this.relations.set(heir, { state: 'kept', held: record.held, vouched: record.vouched, open: record.open, offered: record.offered });
61
+ return true;
62
+ }
63
+
64
+ async arrive(bytes: Uint8Array): Promise<Judged> {
65
+ const stranger = async (n: number): Promise<Judged> => ({ bytes: await this.#silence(bytes), case: n, heard: false });
66
+ if (bytes.length > SIZE || bytes.length < HEAD) return stranger(1);
67
+ const agreement = await this.key.open(bytes.subarray(0, HEAD));
68
+ const opened = agreement && (await openHead(bytes, agreement));
69
+ if (!opened) return stranger(1);
70
+ const isZero = zero(opened.head);
71
+ const heir = hex(opened.head);
72
+ const record = isZero ? undefined : this.relations.get(heir);
73
+ const body = await this.#body(opened, record);
74
+ if (!body) return stranger(1);
75
+ const read = readPayload(body.text);
76
+ if ('fault' in read) return stranger(read.fault === 'not an object' ? 2 : 3);
77
+ const p = read.payload;
78
+ if (isZero ? p.to !== null : p.to !== heir) return stranger(3);
79
+
80
+ if (isZero) {
81
+ if (!this.behind.zero) return stranger(4);
82
+ if (!(await checkBody(unhex(p.by), body))) return stranger(5);
83
+ const choice = await this.#choose(null, p);
84
+ return { bytes: await this.#sealed(opened, choice), case: 'object' in choice ? 12 : 13, heard: false };
85
+ }
86
+
87
+ if (record === undefined) return stranger(6);
88
+ if (!admits(record, heir, p.by)) return stranger(7);
89
+ if (!(await checkBody(unhex(p.by), body))) return stranger(8);
90
+ const reply = await replyKey(this.draw, opened.lid);
91
+ if (!reply) return stranger(1);
92
+ const offered = await follow(body.edge, reply.agreement);
93
+
94
+ // Checked again, since another arrival may have moved the keys while
95
+ // this one was checking its signature.
96
+ const now = this.relations.get(heir);
97
+ if (now === undefined || !admits(now, heir, p.by) || !opensUnder(now, body.edge)) return stranger(7);
98
+ if (now.state === 'kept') return this.#heard(reply, { quo: 'removed' }, 9);
99
+ if (now.state === 'fresh' && (p.next === null || p.next === heir)) return this.#heard(reply, { quo: 'unannounced' }, 10);
100
+ if (now.state === 'spent' && !honours(now, p.seq)) return this.#heard(reply, { quo: 'repeated' }, 11);
101
+
102
+ this.relations.set(heir, move(now, heir, p, hex(body.edge), hex(offered)));
103
+ const choice = await this.#choose(heir, p);
104
+ const kept = await this.relations.kept().catch(() => false);
105
+ return this.#heard(reply, kept ? choice : { silence: true }, kept && 'object' in choice ? 12 : 13);
106
+ }
107
+
108
+ async #heard(key: ReplyKey, reply: Reply, n: number): Promise<Judged> {
109
+ return { bytes: await sealReply(key, await this.#signed(reply)), case: n, heard: true };
110
+ }
111
+
112
+ #signed(reply: Reply): Promise<Uint8Array> {
113
+ return signBody(utf8(writeReply(reply)), this.key.signing);
114
+ }
115
+
116
+ async #choose(heir: string | null, p: Payload): Promise<Choice> {
117
+ try {
118
+ return await this.behind.answer(heir, p);
119
+ } catch {
120
+ return { silence: true };
121
+ }
122
+ }
123
+
124
+ async #sealed(opened: OpenedHead, reply: Reply): Promise<Uint8Array> {
125
+ const key = await replyKey(this.draw, opened.lid);
126
+ return key ? sealReply(key, await this.#signed(reply)) : this.#silence(opened.lid);
127
+ }
128
+
129
+ // The body, opened under the edge keys the door takes for this head.
130
+ async #body(opened: OpenedHead, record: HeirRecord | undefined): Promise<Body | null> {
131
+ const tries: { edge: Uint8Array; sealed: Uint8Array }[] = [];
132
+ if (record?.state === 'fresh') {
133
+ const knock = splitKnock(opened.rest);
134
+ const lock = this.relations.lock();
135
+ const shared = knock && lock ? decapsulate(lock.dk, knock.ciphertext) : null;
136
+ if (!knock || !shared) return null;
137
+ tries.push({ edge: await knockEdge(shared), sealed: knock.sealedBody });
138
+ } else if (record === undefined) tries.push({ edge: ZERO_EDGE, sealed: opened.rest });
139
+ else for (const edge of [record.open, record.offered]) tries.push({ edge: unhex(edge), sealed: opened.rest });
140
+ for (const { edge, sealed } of tries) {
141
+ const plain = await openBody(opened, edge, sealed);
142
+ if (plain === null) continue;
143
+ const body = splitBody(plain);
144
+ return body && { edge, ...body };
145
+ }
146
+ return null;
147
+ }
148
+
149
+ // A stranger's silence: sealed to the first thirty-two bytes that arrived
150
+ // when they take a seal, and otherwise to a lid nobody holds.
151
+ async #silence(arrived: Uint8Array): Promise<Uint8Array> {
152
+ const text = await this.#signed({ silence: true });
153
+ const candidates = [arrived.length >= HEAD ? arrived.subarray(0, HEAD) : null, this.draw(HEAD)];
154
+ for (const lid of candidates) {
155
+ const key = lid && (await replyKey(this.draw, lid));
156
+ if (key) return sealReply(key, text);
157
+ }
158
+ return sealReply((await replyKey(this.draw, await x25519Public(this.draw(HEAD))))!, text);
159
+ }
160
+ }
161
+
162
+ const admits = (record: HeirRecord, heir: string, by: string): boolean => (record.state === 'fresh' ? by === heir : by === record.held || by === record.vouched);
163
+
164
+ const opensUnder = (record: HeirRecord, edge: Uint8Array): boolean => record.state === 'fresh' || hex(edge) === record.open || hex(edge) === record.offered;
165
+
166
+ const honours = (record: { highest: number; honoured: readonly number[] }, seq: number): boolean =>
167
+ seq > record.highest || (seq > record.highest - SPAN && seq !== record.highest && !record.honoured.includes(seq));
168
+
169
+ // The move tables: the keys and edge keys after a choice, and the count.
170
+ const move = (record: HeirRecord, heir: string, p: Payload, edge: string, offered: string): HeirRecord => {
171
+ const announced = p.next !== null && p.next !== heir && p.next !== p.by ? p.next : null;
172
+ if (record.state === 'fresh') return { state: 'spent', held: announced!, vouched: null, open: edge, offered, highest: p.seq, honoured: [] };
173
+ if (record.state !== 'spent') return record;
174
+ if (p.seq < record.highest) return { ...record, honoured: [...record.honoured, p.seq] };
175
+ const keys: Keys = p.by === record.held ? { held: record.held, vouched: announced ?? record.vouched, open: edge, offered } : { held: p.by, vouched: announced, open: edge, offered };
176
+ const honoured = [...record.honoured, record.highest].filter((n) => n > p.seq - SPAN);
177
+ return { state: 'spent', ...keys, highest: p.seq, honoured };
178
+ };
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Quo's wire: keys, the invitation, the payload, the reply, the boxes, the
3
+ // door and the standing. It knows no being, no ward and no terrain.
4
+ export { Door, SPAN, type Behind, type Choice, type Judged } from './door.ts';
5
+ export { readInvitation, type Invitation } from './invitation.ts';
6
+ export { Ephemeral, SigningKey, WardKey, type Draw } from './keys.ts';
7
+ export { readPayload, writePayload, type Payload } from './payload.ts';
8
+ export { GONE, keptLock, lockOf, MemoryRelations, type HeirRecord, type KeptLock, type Relations } from './relations.ts';
9
+ export { readReply, SILENCE, WORDS, writeReply, type Reply, type Word } from './reply.ts';
10
+ export { SIZE } from './seal.ts';
11
+ export { freshStanding, Standing, type Read, type Sent, type StandingState } from './standing.ts';
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The invitation: `{ ward, heir, secret, lock }`, a value with no route.
3
+ import { EK, encapsulate, isHex, unhex } from '../crypto/index.ts';
4
+
5
+ export type Invitation = { readonly ward: string; readonly heir: string; readonly secret: string; readonly lock: string };
6
+
7
+ // An invitation, or null for any other shape. A field beside the four is
8
+ // ignored, and the lock is held to the modulus check.
9
+ export const readInvitation = (value: unknown): Invitation | null => {
10
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
11
+ const { ward, heir, secret, lock } = value as Record<string, unknown>;
12
+ if (!isHex(ward, 64) || !isHex(heir, 32) || !isHex(secret, 32) || !isHex(lock, EK)) return null;
13
+ if (encapsulate(unhex(lock), new Uint8Array(32)) === null) return null;
14
+ return { ward, heir, secret, lock };
15
+ };
@@ -0,0 +1,91 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The keys of Quo: the ward key derived from a seed, and every other key
3
+ // drawn. A draw is the caller's, so this layer names no source of bytes.
4
+ import { agree, ed25519Public, hex, hkdf, lockFrom, sha256, utf8, x25519Public, type Lock } from '../crypto/index.ts';
5
+
6
+ // Drawn bytes, from whatever the caller's terrain gives.
7
+ export type Draw = (length: number) => Uint8Array;
8
+
9
+ export const LABEL = {
10
+ seal: utf8('quo-seal'),
11
+ edgeSeal: utf8('quo-edge-seal'),
12
+ lock: utf8('quo-lock'),
13
+ edge: utf8('quo-edge'),
14
+ wardSign: utf8('quo-ward-sign'),
15
+ wardSeal: utf8('quo-ward-seal'),
16
+ } as const;
17
+
18
+ // An Ed25519 pair: an heir, or a standing's own key.
19
+ export class SigningKey {
20
+ readonly secret: Uint8Array;
21
+ readonly pk: Uint8Array;
22
+ private constructor(secret: Uint8Array, pk: Uint8Array) {
23
+ this.secret = secret;
24
+ this.pk = pk;
25
+ }
26
+ static async from(secret: Uint8Array): Promise<SigningKey> {
27
+ return new SigningKey(secret, await ed25519Public(secret));
28
+ }
29
+ static draw(draw: Draw): Promise<SigningKey> {
30
+ return SigningKey.from(draw(32));
31
+ }
32
+ get id(): string {
33
+ return hex(this.pk);
34
+ }
35
+ }
36
+
37
+ // An X25519 pair used for one box.
38
+ export class Ephemeral {
39
+ readonly secret: Uint8Array;
40
+ readonly pk: Uint8Array;
41
+ private constructor(secret: Uint8Array, pk: Uint8Array) {
42
+ this.secret = secret;
43
+ this.pk = pk;
44
+ }
45
+ static async from(secret: Uint8Array): Promise<Ephemeral> {
46
+ return new Ephemeral(secret, await x25519Public(secret));
47
+ }
48
+ static draw(draw: Draw): Promise<Ephemeral> {
49
+ return Ephemeral.from(draw(32));
50
+ }
51
+ agree(pk: Uint8Array): Promise<Uint8Array | null> {
52
+ return agree(this.secret, pk);
53
+ }
54
+ }
55
+
56
+ // The ward key: a signing pair and a padlock, both from the seed.
57
+ export class WardKey {
58
+ readonly signing: SigningKey;
59
+ readonly sealSecret: Uint8Array;
60
+ readonly padlock: Uint8Array;
61
+ private constructor(signing: SigningKey, sealSecret: Uint8Array, padlock: Uint8Array) {
62
+ this.signing = signing;
63
+ this.sealSecret = sealSecret;
64
+ this.padlock = padlock;
65
+ }
66
+
67
+ // Thirty-two bytes are the seed. Text, and bytes of any other length, are
68
+ // hashed to it first.
69
+ static async from(seed: Uint8Array | string): Promise<WardKey> {
70
+ const bytes = typeof seed === 'string' ? await sha256(utf8(seed)) : seed.length === 32 ? seed : await sha256(seed);
71
+ const sealSecret = await hkdf(bytes, LABEL.wardSeal, 32);
72
+ return new WardKey(await SigningKey.from(await hkdf(bytes, LABEL.wardSign, 32)), sealSecret, await x25519Public(sealSecret));
73
+ }
74
+
75
+ get pk(): string {
76
+ return hex(this.signing.pk) + hex(this.padlock);
77
+ }
78
+
79
+ open(lid: Uint8Array): Promise<Uint8Array | null> {
80
+ return agree(this.sealSecret, lid);
81
+ }
82
+ }
83
+
84
+ // A ward pk's two halves.
85
+ export const wardHalves = (pk: string): { signing: string; padlock: string } => ({ signing: pk.slice(0, 64), padlock: pk.slice(64) });
86
+
87
+ // A lock from sixty-four drawn bytes.
88
+ export const drawLock = (draw: Draw): Lock => {
89
+ const bytes = draw(64);
90
+ return lockFrom(bytes.subarray(0, 32), bytes.subarray(32));
91
+ };
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The payload: `{ to, by, next, seq, method?, args? }`. `args` is carried as
3
+ // the text it was written in, since Quo reads nothing inside it.
4
+ import { isCount, isHex, readObject } from '../crypto/index.ts';
5
+
6
+ export type Payload = {
7
+ readonly to: string | null;
8
+ readonly by: string;
9
+ readonly next: string | null;
10
+ readonly seq: number;
11
+ readonly method?: string;
12
+ readonly args?: string;
13
+ };
14
+
15
+ const OWED = ['to', 'by', 'next', 'seq'];
16
+ const ZERO = '0'.repeat(64);
17
+ const isPk = (text: string): boolean => isHex(text, 32) && text !== ZERO;
18
+
19
+ // A pk written as a JSON string, or null written as null.
20
+ const pkOrNull = (text: string, nullable: boolean): string | null | undefined => {
21
+ if (nullable && text === 'null') return null;
22
+ if (!/^"[0-9a-f]{64}"$/.test(text)) return undefined;
23
+ const pk = text.slice(1, -1);
24
+ return isPk(pk) ? pk : undefined;
25
+ };
26
+
27
+ export const writePayload = (p: Payload): string => {
28
+ const parts = [`"to":${JSON.stringify(p.to)}`, `"by":"${p.by}"`, `"next":${JSON.stringify(p.next)}`, `"seq":${p.seq}`];
29
+ if (p.method !== undefined) parts.push(`"method":${JSON.stringify(p.method)}`);
30
+ if (p.args !== undefined) parts.push(`"args":${p.args}`);
31
+ return `{${parts.join(',')}}`;
32
+ };
33
+
34
+ export type PayloadRead = { payload: Payload } | { fault: 'not an object' | 'not well formed' };
35
+
36
+ // Cases 2 and 3 of the door, before the head is compared.
37
+ export const readPayload = (bytes: Uint8Array): PayloadRead => {
38
+ const fields = readObject(bytes);
39
+ if (fields === null) return { fault: 'not an object' };
40
+ const bad = { fault: 'not well formed' } as const;
41
+ if (!OWED.every((f) => fields.has(f))) return bad;
42
+ const to = pkOrNull(fields.get('to')!, true);
43
+ const by = pkOrNull(fields.get('by')!, false);
44
+ const next = pkOrNull(fields.get('next')!, true);
45
+ const seq = fields.get('seq')!;
46
+ if (to === undefined || !by || next === undefined || !isCount(seq)) return bad;
47
+ const method = fields.get('method');
48
+ if (method !== undefined && !method.startsWith('"')) return bad;
49
+ const args = fields.get('args');
50
+ if (args !== undefined && !args.startsWith('{')) return bad;
51
+ return {
52
+ payload: {
53
+ to,
54
+ by,
55
+ next,
56
+ seq: Number(seq),
57
+ ...(method === undefined ? {} : { method: JSON.parse(method) as string }),
58
+ ...(args === undefined ? {} : { args }),
59
+ },
60
+ };
61
+ };
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // What a door keeps: its lock, and for each heir it holds the keys and the
3
+ // count of the spec's relation chapter. Keys are written as hex, so a record
4
+ // is a value any memory can keep.
5
+ import type { Lock } from '../crypto/index.ts';
6
+ import { hex, unhex } from '../crypto/index.ts';
7
+
8
+ export type Keys = { readonly held: string; readonly vouched: string | null; readonly open: string; readonly offered: string };
9
+
10
+ export type HeirRecord =
11
+ | { readonly state: 'fresh' }
12
+ | ({ readonly state: 'spent'; readonly highest: number; readonly honoured: readonly number[] } & Keys)
13
+ | ({ readonly state: 'kept' } & Keys);
14
+
15
+ export type KeptLock = { readonly ek: string; readonly dk: string };
16
+
17
+ // The door's memory. A ward stands it on its partition; `MemoryRelations`
18
+ // is the one that lives in an object.
19
+ export interface Relations {
20
+ lock(): Lock | undefined;
21
+ setLock(lock: Lock): void;
22
+ get(heir: string): HeirRecord | undefined;
23
+ set(heir: string, record: HeirRecord): void;
24
+ delete(heir: string): void;
25
+ // Whether what was written during an arrival is kept, asked once before
26
+ // the reply is sealed.
27
+ kept(): Promise<boolean>;
28
+ }
29
+
30
+ export const lockOf = (kept: KeptLock | undefined): Lock | undefined => (kept ? { ek: unhex(kept.ek), dk: unhex(kept.dk) } : undefined);
31
+ export const keptLock = (lock: Lock): KeptLock => ({ ek: hex(lock.ek), dk: hex(lock.dk) });
32
+
33
+ // A bound on the kept records of removed relations, oldest out.
34
+ export const GONE = 256;
35
+
36
+ export class MemoryRelations implements Relations {
37
+ #lock: KeptLock | undefined;
38
+ readonly #heirs = new Map<string, HeirRecord>();
39
+ readonly #gone: string[] = [];
40
+
41
+ lock(): Lock | undefined {
42
+ return lockOf(this.#lock);
43
+ }
44
+ setLock(lock: Lock): void {
45
+ this.#lock = keptLock(lock);
46
+ }
47
+ get(heir: string): HeirRecord | undefined {
48
+ return this.#heirs.get(heir);
49
+ }
50
+ set(heir: string, record: HeirRecord): void {
51
+ this.#heirs.set(heir, record);
52
+ if (record.state !== 'kept') return;
53
+ this.#gone.push(heir);
54
+ while (this.#gone.length > GONE) this.#heirs.delete(this.#gone.shift()!);
55
+ }
56
+ delete(heir: string): void {
57
+ this.#heirs.delete(heir);
58
+ }
59
+ kept(): Promise<boolean> {
60
+ return Promise.resolve(true);
61
+ }
62
+ }