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
@@ -0,0 +1,134 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A harbor on a disk, for local proof: `nervur/folder`, the one entry that
3
+ // names a platform.
4
+ //
5
+ // <root>/<ward>/seed the ward's seed, as hex
6
+ // <root>/<ward>/ward.json the ward's head
7
+ // <root>/<ward>/<key>/being.json one being: class, cells, relations
8
+ //
9
+ // A folder name is the ward's name or the being's key, escaped, so no name
10
+ // reaches outside its folder. A file is written whole, beside itself and
11
+ // then renamed over, so a crash leaves the last kept copy. Seeds are plain
12
+ // files: custody that seals them is a terrain's, in the dock.
13
+ import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { hex, isHex, unhex } from '../crypto/index.ts';
16
+ import { Custody, Memory, type Entropy, type Rows } from '../contract/index.ts';
17
+ import { HEAD_ROW } from '../ward/index.ts';
18
+
19
+ const HEAD = 'ward.json';
20
+ const BEING = 'being.json';
21
+
22
+ export const folderName = (name: string): string => encodeURIComponent(name).replaceAll('.', '%2E').replaceAll('*', '%2A') || '%';
23
+ const nameOf = (folder: string): string => (folder === '%' ? '' : decodeURIComponent(folder));
24
+
25
+ const missing = (e: unknown): boolean => (e as { code?: string }).code === 'ENOENT';
26
+
27
+ const readJson = async (path: string): Promise<unknown> => {
28
+ try {
29
+ return JSON.parse(await readFile(path, 'utf8')) as unknown;
30
+ } catch (e) {
31
+ if (missing(e)) return undefined;
32
+ throw e;
33
+ }
34
+ };
35
+
36
+ const writeWhole = async (path: string, text: string): Promise<void> => {
37
+ await writeFile(`${path}.next`, text, { mode: 0o600 });
38
+ await rename(`${path}.next`, path);
39
+ };
40
+
41
+ class FolderRows implements Rows {
42
+ readonly rows: Record<string, unknown>;
43
+ readonly #dir: string;
44
+ readonly #told = new Set<string>();
45
+ #line: Promise<boolean> = Promise.resolve(true);
46
+
47
+ constructor(dir: string, rows: Record<string, unknown>) {
48
+ this.#dir = dir;
49
+ this.rows = rows;
50
+ }
51
+
52
+ told(row: string): void {
53
+ this.#told.add(row);
54
+ }
55
+
56
+ // Every row told since the last keep, written in order, one keep at a time.
57
+ kept(): Promise<boolean> {
58
+ const rows = [...this.#told];
59
+ this.#told.clear();
60
+ this.#line = this.#line.then(() => this.#write(rows)).catch(() => false);
61
+ return this.#line;
62
+ }
63
+
64
+ async #write(told: string[]): Promise<boolean> {
65
+ await mkdir(this.#dir, { recursive: true, mode: 0o700 });
66
+ const beings = (this.rows.beings ?? {}) as Record<string, unknown>;
67
+ for (const row of told) {
68
+ if (row === HEAD_ROW) {
69
+ await writeWhole(join(this.#dir, HEAD), JSON.stringify(this.rows.ward ?? null));
70
+ continue;
71
+ }
72
+ const dir = join(this.#dir, folderName(row));
73
+ if (!Object.hasOwn(beings, row)) {
74
+ await rm(dir, { recursive: true, force: true });
75
+ continue;
76
+ }
77
+ await mkdir(dir, { recursive: true, mode: 0o700 });
78
+ await writeWhole(join(dir, BEING), JSON.stringify(beings[row]));
79
+ }
80
+ return true;
81
+ }
82
+ }
83
+
84
+ export class FolderMemory extends Memory {
85
+ readonly root: string;
86
+
87
+ constructor(root: string) {
88
+ super();
89
+ this.root = root;
90
+ }
91
+
92
+ // The rows as the disk holds them now.
93
+ async open(name: string): Promise<Rows> {
94
+ const dir = join(this.root, folderName(name));
95
+ const rows: Record<string, unknown> = {};
96
+ const head = await readJson(join(dir, HEAD));
97
+ if (head !== undefined) rows.ward = head;
98
+ const beings: Record<string, unknown> = {};
99
+ const entries = await readdir(dir, { withFileTypes: true }).catch((e: unknown) => (missing(e) ? [] : Promise.reject(e)));
100
+ for (const entry of entries) {
101
+ if (!entry.isDirectory()) continue;
102
+ const row = await readJson(join(dir, entry.name, BEING));
103
+ if (row !== undefined) beings[nameOf(entry.name)] = row;
104
+ }
105
+ if (head !== undefined || Object.keys(beings).length > 0) rows.beings = beings;
106
+ return new FolderRows(dir, rows);
107
+ }
108
+ }
109
+
110
+ export class FolderCustody extends Custody {
111
+ readonly root: string;
112
+ readonly #entropy: Entropy;
113
+
114
+ constructor(root: string, entropy: Entropy) {
115
+ super();
116
+ this.root = root;
117
+ this.#entropy = entropy;
118
+ }
119
+
120
+ async seed(name: string): Promise<Uint8Array> {
121
+ const dir = join(this.root, folderName(name));
122
+ const path = join(dir, 'seed');
123
+ const kept = await readFile(path, 'utf8').catch((e: unknown) => (missing(e) ? undefined : Promise.reject(e)));
124
+ if (kept !== undefined) {
125
+ const text = kept.trim();
126
+ if (!isHex(text, 32)) throw new Error(`${path} is not a seed`);
127
+ return unhex(text);
128
+ }
129
+ const seed = this.#entropy.draw(32);
130
+ await mkdir(dir, { recursive: true, mode: 0o700 });
131
+ await writeFile(path, `${hex(seed)}\n`, { mode: 0o600, flag: 'wx' });
132
+ return seed;
133
+ }
134
+ }
@@ -0,0 +1,101 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The dock: the box ward's ward-being. Beside the root's asks of every ward,
3
+ // she opens faculties, hosts wards, and lends each contract a faculty
4
+ // fulfils to the wards she hosts.
5
+ //
6
+ // open { key, class } a faculty stands in the box ward, and the
7
+ // dock holds a standing on her under her key
8
+ // host { ward } a hosted ward stands, holding a standing on
9
+ // the dock
10
+ // lend { contract } asked by a hosted ward: an offer from a
11
+ // faculty that fulfils the contract
12
+ // retract { contract, heir } an offer that was not taken
13
+ import { Faculty, type Asker, type AskSpec, type Invitation, type JsonObject } from '../being/index.ts';
14
+ import { readInvitation } from '../quo/index.ts';
15
+ import { ownerAsk, WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
16
+
17
+ // What the dock asks of her harbor.
18
+ export interface Hosting {
19
+ host(name: string, invitation: Invitation): Promise<boolean>;
20
+ }
21
+
22
+ export const BOX = 'box';
23
+ const text = (v: unknown): v is string => typeof v === 'string';
24
+ const hosted: AskSpec['for'] = (_: Asker, notes: JsonObject | undefined) => notes?.hosted === true;
25
+
26
+ export class Dock extends WardBeing implements Lender {
27
+ static override cells: JsonObject = { hosted: [] };
28
+ static override asks: Record<string, AskSpec> = {
29
+ ...WardBeing.asks,
30
+ open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
31
+ host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
32
+ lend: { description: 'an offer from a faculty fulfilling a contract', input: { type: 'object', required: ['contract'] }, for: hosted },
33
+ retract: { description: 'an offer that was not taken', input: { type: 'object', required: ['contract', 'heir'] }, for: hosted },
34
+ };
35
+
36
+ readonly #hosting: Hosting;
37
+
38
+ constructor(stance: WardBeingStance, hosting: Hosting) {
39
+ super(stance);
40
+ this.#hosting = hosting;
41
+ }
42
+
43
+ get hosted(): string[] {
44
+ return [...(this.cells.hosted as string[])];
45
+ }
46
+
47
+ async open(args: JsonObject): Promise<JsonObject> {
48
+ if (!text(args.key) || !text(args.class)) return { error: 'key and class are text' };
49
+ if (!Faculty.fulfils(this.steward.classOf(args.class))) return { error: 'no such faculty' };
50
+ const opened = await this.stance.boot(args.class, args.key, args.key);
51
+ return opened === null ? { error: 'not opened' } : { opened };
52
+ }
53
+
54
+ async host(args: JsonObject): Promise<JsonObject> {
55
+ const name = args.ward;
56
+ if (!text(name) || name === BOX || this.hosted.includes(name)) return { error: 'no such ward to host' };
57
+ const invitation = await this.stance.occupants.invite(name, { hosted: true });
58
+ if (invitation === null) return { error: 'not hosted' };
59
+ if (!(await this.#hosting.host(name, invitation))) {
60
+ this.stance.occupants.remove(name);
61
+ return { error: 'not hosted' };
62
+ }
63
+ (this.cells.hosted as string[]).push(name);
64
+ return { hosted: name };
65
+ }
66
+
67
+ async lend(args: JsonObject): Promise<JsonObject> {
68
+ if (!text(args.contract)) return { error: 'contract is text' };
69
+ const invitation = await this.offer(args.contract);
70
+ return invitation ? { invitation } : { error: 'nothing lent' };
71
+ }
72
+
73
+ async retract(args: JsonObject): Promise<JsonObject> {
74
+ if (!text(args.contract) || !text(args.heir)) return { error: 'contract and heir are text' };
75
+ await this.withdraw(args.contract, args.heir);
76
+ return { retracted: args.heir };
77
+ }
78
+
79
+ // Lender: the first standing faculty that fulfils the contract offers.
80
+ async offer(contract: string): Promise<Invitation | null> {
81
+ for (const key of this.#fulfilling(contract)) {
82
+ const out = await this.stance.standings.get(key)?.ask('offer');
83
+ const invitation = readInvitation((out as { invitation?: unknown } | undefined)?.invitation);
84
+ if (invitation) return invitation;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ async withdraw(contract: string, heir: string): Promise<void> {
90
+ for (const key of this.#fulfilling(contract)) await this.stance.standings.get(key)?.ask('retract', { heir });
91
+ }
92
+
93
+ #fulfilling(contract: string): string[] {
94
+ const beings = this.steward.beings();
95
+ return this.stance.standings.ids().filter((key) => {
96
+ const being = beings[key];
97
+ const C = being && !being.absent ? this.steward.classOf(being.class) : undefined;
98
+ return Faculty.fulfils(C) && Faculty.contracts(C).includes(contract);
99
+ });
100
+ }
101
+ }
@@ -0,0 +1,105 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The harbor: the onion, unpacked from a terrain and a catalogue.
3
+ //
4
+ // 1. custody and memory the box ward's seed and rows, from the terrain
5
+ // 2. the box ward unpacked as every ward is, its ward-being the dock
6
+ // 3. the faculties the box ward's beings, each from her own row
7
+ // 4. the hosted wards as the dock's cells list them
8
+ // 5. the beings in each ward, each from her own row
9
+ //
10
+ // Genesis and wake are one path: empty memory unpacks a box ward holding
11
+ // the dock alone, and the root's asks write the rest.
12
+ import type { Answer, BeingClass, Invitation, JsonObject } from '../being/index.ts';
13
+ import { Carrier } from '../contract/index.ts';
14
+ import { DOCK, Ward, type WardParts } from '../ward/index.ts';
15
+ import { BOX, Dock, type Hosting } from './dock.ts';
16
+ import { Registry } from './registry.ts';
17
+ import type { Terrain } from './terrain.ts';
18
+
19
+ export class Harbor extends Carrier implements Hosting {
20
+ readonly terrain: Terrain;
21
+ readonly registry: Registry;
22
+ readonly #wards = new Map<string, Ward>();
23
+
24
+ private constructor(terrain: Terrain, registry: Registry) {
25
+ super();
26
+ this.terrain = terrain;
27
+ this.registry = registry;
28
+ }
29
+
30
+ static async open(terrain: Terrain, classes: readonly BeingClass[]): Promise<Harbor> {
31
+ const harbor = new Harbor(terrain, new Registry(classes));
32
+ await harbor.#unpack(BOX, undefined, (stance) => new Dock(stance, harbor));
33
+ for (const name of harbor.dock.hosted) await harbor.#unpack(name);
34
+ return harbor;
35
+ }
36
+
37
+ get box(): Ward {
38
+ return this.#wards.get(BOX)!;
39
+ }
40
+
41
+ get dock(): Dock {
42
+ return this.box.wardBeing as Dock;
43
+ }
44
+
45
+ ward(name: string): Ward | undefined {
46
+ return this.#wards.get(name);
47
+ }
48
+
49
+ get names(): string[] {
50
+ return [...this.#wards.keys()].filter((name) => name !== BOX);
51
+ }
52
+
53
+ // The unsealed ask on the box ward: what a command line carries.
54
+ root(method?: string, args?: JsonObject): Promise<Answer> {
55
+ return this.box.root(method, args);
56
+ }
57
+
58
+ holds(pk: string): boolean {
59
+ return this.#door(pk) !== undefined;
60
+ }
61
+
62
+ // Bytes for one of this harbor's doors, or nothing.
63
+ async arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
64
+ const ward = this.#door(pk);
65
+ return ward ? (await ward.door.arrive(bytes)).bytes : null;
66
+ }
67
+
68
+ // Carrier: this harbor's own doors first, then the terrain's.
69
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
70
+ return this.holds(pk) ? this.arrive(pk, bytes) : this.terrain.carrier.carry(pk, bytes);
71
+ }
72
+
73
+ // Hosting: a ward the dock hosts stands now.
74
+ async host(name: string, invitation: Invitation): Promise<boolean> {
75
+ if (this.#wards.has(name)) return false;
76
+ try {
77
+ await this.#unpack(name, invitation);
78
+ } catch {
79
+ return false;
80
+ }
81
+ if (this.#wards.get(name)!.wardBeing.stance.standings.get(DOCK)) return true;
82
+ this.#wards.delete(name);
83
+ return false;
84
+ }
85
+
86
+ async #unpack(name: string, dock?: Invitation, root?: WardParts['root']): Promise<void> {
87
+ const { entropy, clock, custody, memory } = this.terrain;
88
+ const ward = await Ward.unpack({
89
+ seed: await custody.seed(name),
90
+ memory: await memory.open(name),
91
+ catalogue: this.registry,
92
+ entropy,
93
+ clock,
94
+ carrier: this,
95
+ ...(dock ? { dock } : {}),
96
+ ...(root ? { root } : {}),
97
+ });
98
+ this.#wards.set(name, ward);
99
+ }
100
+
101
+ #door(pk: string): Ward | undefined {
102
+ for (const ward of this.#wards.values()) if (ward.pk === pk) return ward;
103
+ return undefined;
104
+ }
105
+ }
@@ -1,20 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // nervur/harbor — the Ground contract and the harbors. The only
3
- // place in the package that may know a wire, and it knows one on the
4
- // standard surface alone: fetch and WebSocket. MemoryHarbor knows none: it
5
- // is one process talking to itself, where every ward assertion is made.
6
- export { MemoryHarbor, type Booted, type WardFactory } from './memory.ts';
7
- export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './core.ts';
8
- export { dial, type Dialer } from './dial.ts';
9
- export { MemoryStore, values, rowsOf, type Store, type Kept, type WardRecord } from './store.ts';
10
- // What a store keeping a ward in rows needs: the name of the row that is not
11
- // a being, and the partition back from the rows it was cut into.
12
- export { HEAD, fromRows, rowsIn } from '../ward/partition.ts';
13
- export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
14
- export type { Ground, WardPointers, Memory } from '../ward/ground.ts';
15
- export { BOX, OFFER, RETRACT, volatile } from '../ward/ground.ts';
16
- // The box as one being, and the acts a harbor runs to stand it.
17
- export { Box, BOX_SEED, BOX_ASKS, stand, hold, offerTo, roll } from './box.ts';
18
- // What a harbor builds a ground out of. Convenience, never contract: a kit
19
- // writing its own harbor may write these three again.
20
- export { maker, entropy, learnPk } from '../ward/ground.ts';
2
+ // The onion: a terrain, a registry, the dock, and the harbor.
3
+ export { BOX, Dock, type Hosting } from './dock.ts';
4
+ export { Harbor } from './harbor.ts';
5
+ export { Registry } from './registry.ts';
6
+ export { Terrain } from './terrain.ts';
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The catalogue a program hands its harbor, indexed by class name. It
3
+ // resolves a name a row keeps, and decides nothing about what stands.
4
+ import type { BeingClass } from '../being/index.ts';
5
+ import type { Catalogue } from '../ward/index.ts';
6
+
7
+ export class Registry implements Catalogue {
8
+ readonly #classes = new Map<string, BeingClass>();
9
+
10
+ constructor(classes: readonly BeingClass[]) {
11
+ for (const C of classes) {
12
+ if (this.#classes.has(C.name) && this.#classes.get(C.name) !== C) throw new Error(`two classes are named ${C.name}`);
13
+ this.#classes.set(C.name, C);
14
+ }
15
+ }
16
+
17
+ classOf(name: string): BeingClass | undefined {
18
+ return this.#classes.get(name);
19
+ }
20
+ }
@@ -0,0 +1,13 @@
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, 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
+ // What reaches a ward pk this harbor does not hold.
12
+ abstract readonly carrier: Carrier;
13
+ }
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, Memory, type Rows } from './contract/index.ts';
12
+
13
+ // Wards and the ward-being.
14
+ export { CEILING, DEFAULT, DEPTH, ownerAsk, pilots, WardBeing, type Steward, type WardBeingStance } from './ward/index.ts';
15
+
16
+ // The onion.
17
+ export { BOX, Dock, Harbor, Registry, Terrain } from './harbor/index.ts';
18
+
19
+ // The pointer dock: a whole world in one process.
20
+ export { CryptoEntropy, HeldCustody, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World, type PointerParts } from './pointer/index.ts';
@@ -0,0 +1,47 @@
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, Memory, type Rows } 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
+ // Seeds drawn once per name and held by this object.
24
+ export class HeldCustody extends Custody {
25
+ readonly #entropy: Entropy;
26
+ readonly #seeds = new Map<string, Uint8Array>();
27
+ constructor(entropy: Entropy) {
28
+ super();
29
+ this.#entropy = entropy;
30
+ }
31
+ seed(name: string): Promise<Uint8Array> {
32
+ let seed = this.#seeds.get(name);
33
+ if (!seed) this.#seeds.set(name, (seed = this.#entropy.draw(32)));
34
+ return Promise.resolve(seed);
35
+ }
36
+ }
37
+
38
+ // Rows that live as long as this object: opening a name again gives the
39
+ // same rows, which is what a restart in one process finds.
40
+ export class VolatileMemory extends Memory {
41
+ readonly #wards = new Map<string, Rows>();
42
+ open(name: string): Promise<Rows> {
43
+ let rows = this.#wards.get(name);
44
+ if (!rows) this.#wards.set(name, (rows = { rows: {}, told: () => {}, kept: () => Promise.resolve(true) }));
45
+ return Promise.resolve(rows);
46
+ }
47
+ }
@@ -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, SystemClock, VolatileMemory } from './bodies.ts';
4
+ export { NoCarrier, PointerCarrier, PointerTerrain, World, type PointerParts } from './world.ts';
@@ -0,0 +1,91 @@
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 type { BeingClass } from '../being/index.ts';
7
+ import { Carrier, type Clock, type Custody, type Entropy, type Memory } from '../contract/index.ts';
8
+ import { Harbor, Terrain } from '../harbor/index.ts';
9
+ import { CryptoEntropy, HeldCustody, SystemClock, VolatileMemory } from './bodies.ts';
10
+
11
+ // A carrier that reaches no one: a harbor alone.
12
+ export class NoCarrier extends Carrier {
13
+ carry(): Promise<Uint8Array | null> {
14
+ return Promise.resolve(null);
15
+ }
16
+ }
17
+
18
+ // Bytes to whichever harbor of a world holds the ward pk.
19
+ export class PointerCarrier extends Carrier {
20
+ readonly #world: World;
21
+ constructor(world: World) {
22
+ super();
23
+ this.#world = world;
24
+ }
25
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
26
+ return this.#world.carry(pk, bytes);
27
+ }
28
+ }
29
+
30
+ export type PointerParts = { entropy?: Entropy; clock?: Clock; custody?: Custody; memory?: Memory; carrier?: Carrier };
31
+
32
+ export class PointerTerrain extends Terrain {
33
+ readonly entropy: Entropy;
34
+ readonly clock: Clock;
35
+ readonly custody: Custody;
36
+ readonly memory: Memory;
37
+ readonly carrier: Carrier;
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
+ }
47
+ }
48
+
49
+ type Place = { harbor: Harbor; terrain: Terrain; classes: readonly BeingClass[] };
50
+
51
+ export class World {
52
+ readonly #places = new Map<string, Place>();
53
+ // Harbors the world reaches no one in, nor anyone out of.
54
+ readonly cut = new Set<string>();
55
+
56
+ // A harbor of this world. `parts` replaces the pointer bodies, a folder's
57
+ // memory and custody among them.
58
+ async harbor(name: string, classes: readonly BeingClass[], parts: Omit<PointerParts, 'carrier'> = {}): Promise<Harbor> {
59
+ if (this.#places.has(name)) throw new Error(`a harbor named ${name} stands`);
60
+ const terrain = new PointerTerrain({ ...parts, carrier: new PointerCarrier(this) });
61
+ const harbor = await Harbor.open(terrain, classes);
62
+ this.#places.set(name, { harbor, terrain, classes });
63
+ return harbor;
64
+ }
65
+
66
+ get(name: string): Harbor {
67
+ const place = this.#places.get(name);
68
+ if (!place) throw new Error(`no harbor named ${name}`);
69
+ return place.harbor;
70
+ }
71
+
72
+ // The harbor opened again on its own terrain, with the classes it was
73
+ // made with unless others are named. `parts` replaces bodies of the
74
+ // terrain, as a new process would build them over the same disk.
75
+ async restart(name: string, classes?: readonly BeingClass[], parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor> {
76
+ const place = this.#places.get(name);
77
+ if (!place) throw new Error(`no harbor named ${name}`);
78
+ const t = place.terrain;
79
+ const terrain = parts ? new PointerTerrain({ entropy: t.entropy, clock: t.clock, custody: t.custody, memory: t.memory, ...parts, carrier: t.carrier }) : t;
80
+ const harbor = await Harbor.open(terrain, classes ?? place.classes);
81
+ this.#places.set(name, { ...place, harbor, terrain });
82
+ return harbor;
83
+ }
84
+
85
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
86
+ for (const [name, { harbor }] of this.#places) {
87
+ if (harbor.holds(pk)) return this.cut.has(name) ? Promise.resolve(null) : harbor.arrive(pk, bytes);
88
+ }
89
+ return Promise.resolve(null);
90
+ }
91
+ }