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,29 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Web Crypto, read at every use. A page on a plain http origin has `crypto`
3
+ // without `subtle`, and a terrain may install it after this module loads.
4
+ export const subtle = () => {
5
+ const found = globalThis.crypto?.subtle;
6
+ if (!found)
7
+ throw new Error('this terrain has no crypto.subtle: a ward needs a secure context');
8
+ return found;
9
+ };
10
+ // Web Crypto takes a buffer it owns, so every input is copied into one.
11
+ export const own = (bytes) => Uint8Array.from(bytes);
12
+ // The PKCS#8 wrapping of a thirty-two byte secret, which is the whole of it
13
+ // for both curves.
14
+ const PKCS8 = { Ed25519: '302e020100300506032b657004220420', X25519: '302e020100300506032b656e04220420' };
15
+ export const importSecret = (curve, secret, uses) => {
16
+ if (secret.length !== 32)
17
+ throw new TypeError('a secret is thirty-two bytes');
18
+ const prefix = Uint8Array.from(PKCS8[curve].match(/../g), (b) => parseInt(b, 16));
19
+ const wrapped = new Uint8Array(48);
20
+ wrapped.set(prefix);
21
+ wrapped.set(secret, 16);
22
+ return subtle().importKey('pkcs8', wrapped, { name: curve }, true, uses);
23
+ };
24
+ // Web Crypto gives a private key's public half only through a JWK.
25
+ export const publicOf = async (key) => {
26
+ const { x } = await subtle().exportKey('jwk', key);
27
+ const binary = atob(x.replaceAll('-', '+').replaceAll('_', '/'));
28
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
29
+ };
@@ -0,0 +1,2 @@
1
+ export declare const x25519Public: (secret: Uint8Array) => Promise<Uint8Array>;
2
+ export declare const agree: (secret: Uint8Array, pk: Uint8Array) => Promise<Uint8Array | null>;
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // X25519 agrees. A secret is thirty-two bytes, clamped by the function and
3
+ // never before.
4
+ import { zero } from './bytes.js';
5
+ import { importSecret, own, publicOf, subtle } from './subtle.js';
6
+ export const x25519Public = async (secret) => publicOf(await importSecret('X25519', secret, ['deriveBits']));
7
+ // The agreement, or null for a public key that takes no seal: one whose
8
+ // agreement is thirty-two zero bytes, or one the terrain refuses to read.
9
+ export const agree = async (secret, pk) => {
10
+ if (pk.length !== 32)
11
+ return null;
12
+ try {
13
+ const mine = await importSecret('X25519', secret, ['deriveBits']);
14
+ const theirs = await subtle().importKey('raw', own(pk), { name: 'X25519' }, false, []);
15
+ const shared = new Uint8Array(await subtle().deriveBits({ name: 'X25519', public: theirs }, mine, 256));
16
+ return zero(shared) ? null : shared;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ };
@@ -0,0 +1,13 @@
1
+ import { Custody, Memory, type Entropy, type Rows } from '../contract/index.ts';
2
+ export declare const folderName: (name: string) => string;
3
+ export declare class FolderMemory extends Memory {
4
+ readonly root: string;
5
+ constructor(root: string);
6
+ open(name: string): Promise<Rows>;
7
+ }
8
+ export declare class FolderCustody extends Custody {
9
+ #private;
10
+ readonly root: string;
11
+ constructor(root: string, entropy: Entropy);
12
+ seed(name: string): Promise<Uint8Array>;
13
+ }
@@ -0,0 +1,125 @@
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.js';
16
+ import { Custody, Memory } from '../contract/index.js';
17
+ import { HEAD_ROW } from '../ward/index.js';
18
+ const HEAD = 'ward.json';
19
+ const BEING = 'being.json';
20
+ export const folderName = (name) => encodeURIComponent(name).replaceAll('.', '%2E').replaceAll('*', '%2A') || '%';
21
+ const nameOf = (folder) => (folder === '%' ? '' : decodeURIComponent(folder));
22
+ const missing = (e) => e.code === 'ENOENT';
23
+ const readJson = async (path) => {
24
+ try {
25
+ return JSON.parse(await readFile(path, 'utf8'));
26
+ }
27
+ catch (e) {
28
+ if (missing(e))
29
+ return undefined;
30
+ throw e;
31
+ }
32
+ };
33
+ const writeWhole = async (path, text) => {
34
+ await writeFile(`${path}.next`, text, { mode: 0o600 });
35
+ await rename(`${path}.next`, path);
36
+ };
37
+ class FolderRows {
38
+ rows;
39
+ #dir;
40
+ #told = new Set();
41
+ #line = Promise.resolve(true);
42
+ constructor(dir, rows) {
43
+ this.#dir = dir;
44
+ this.rows = rows;
45
+ }
46
+ told(row) {
47
+ this.#told.add(row);
48
+ }
49
+ // Every row told since the last keep, written in order, one keep at a time.
50
+ kept() {
51
+ const rows = [...this.#told];
52
+ this.#told.clear();
53
+ this.#line = this.#line.then(() => this.#write(rows)).catch(() => false);
54
+ return this.#line;
55
+ }
56
+ async #write(told) {
57
+ await mkdir(this.#dir, { recursive: true, mode: 0o700 });
58
+ const beings = (this.rows.beings ?? {});
59
+ for (const row of told) {
60
+ if (row === HEAD_ROW) {
61
+ await writeWhole(join(this.#dir, HEAD), JSON.stringify(this.rows.ward ?? null));
62
+ continue;
63
+ }
64
+ const dir = join(this.#dir, folderName(row));
65
+ if (!Object.hasOwn(beings, row)) {
66
+ await rm(dir, { recursive: true, force: true });
67
+ continue;
68
+ }
69
+ await mkdir(dir, { recursive: true, mode: 0o700 });
70
+ await writeWhole(join(dir, BEING), JSON.stringify(beings[row]));
71
+ }
72
+ return true;
73
+ }
74
+ }
75
+ export class FolderMemory extends Memory {
76
+ root;
77
+ constructor(root) {
78
+ super();
79
+ this.root = root;
80
+ }
81
+ // The rows as the disk holds them now.
82
+ async open(name) {
83
+ const dir = join(this.root, folderName(name));
84
+ const rows = {};
85
+ const head = await readJson(join(dir, HEAD));
86
+ if (head !== undefined)
87
+ rows.ward = head;
88
+ const beings = {};
89
+ const entries = await readdir(dir, { withFileTypes: true }).catch((e) => (missing(e) ? [] : Promise.reject(e)));
90
+ for (const entry of entries) {
91
+ if (!entry.isDirectory())
92
+ continue;
93
+ const row = await readJson(join(dir, entry.name, BEING));
94
+ if (row !== undefined)
95
+ beings[nameOf(entry.name)] = row;
96
+ }
97
+ if (head !== undefined || Object.keys(beings).length > 0)
98
+ rows.beings = beings;
99
+ return new FolderRows(dir, rows);
100
+ }
101
+ }
102
+ export class FolderCustody extends Custody {
103
+ root;
104
+ #entropy;
105
+ constructor(root, entropy) {
106
+ super();
107
+ this.root = root;
108
+ this.#entropy = entropy;
109
+ }
110
+ async seed(name) {
111
+ const dir = join(this.root, folderName(name));
112
+ const path = join(dir, 'seed');
113
+ const kept = await readFile(path, 'utf8').catch((e) => (missing(e) ? undefined : Promise.reject(e)));
114
+ if (kept !== undefined) {
115
+ const text = kept.trim();
116
+ if (!isHex(text, 32))
117
+ throw new Error(`${path} is not a seed`);
118
+ return unhex(text);
119
+ }
120
+ const seed = this.#entropy.draw(32);
121
+ await mkdir(dir, { recursive: true, mode: 0o700 });
122
+ await writeFile(path, `${hex(seed)}\n`, { mode: 0o600, flag: 'wx' });
123
+ return seed;
124
+ }
125
+ }
@@ -0,0 +1,19 @@
1
+ import { type AskSpec, type Invitation, type JsonObject } from '../being/index.ts';
2
+ import { WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
3
+ export interface Hosting {
4
+ host(name: string, invitation: Invitation): Promise<boolean>;
5
+ }
6
+ export declare const BOX = "box";
7
+ export declare class Dock extends WardBeing implements Lender {
8
+ #private;
9
+ static cells: JsonObject;
10
+ static asks: Record<string, AskSpec>;
11
+ constructor(stance: WardBeingStance, hosting: Hosting);
12
+ get hosted(): string[];
13
+ open(args: JsonObject): Promise<JsonObject>;
14
+ host(args: JsonObject): Promise<JsonObject>;
15
+ lend(args: JsonObject): Promise<JsonObject>;
16
+ retract(args: JsonObject): Promise<JsonObject>;
17
+ offer(contract: string): Promise<Invitation | null>;
18
+ withdraw(contract: string, heir: string): Promise<void>;
19
+ }
@@ -0,0 +1,92 @@
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 } from '../being/index.js';
14
+ import { readInvitation } from '../quo/index.js';
15
+ import { ownerAsk, WardBeing } from '../ward/index.js';
16
+ export const BOX = 'box';
17
+ const text = (v) => typeof v === 'string';
18
+ const hosted = (_, notes) => notes?.hosted === true;
19
+ export class Dock extends WardBeing {
20
+ static cells = { hosted: [] };
21
+ static asks = {
22
+ ...WardBeing.asks,
23
+ open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
24
+ host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
25
+ lend: { description: 'an offer from a faculty fulfilling a contract', input: { type: 'object', required: ['contract'] }, for: hosted },
26
+ retract: { description: 'an offer that was not taken', input: { type: 'object', required: ['contract', 'heir'] }, for: hosted },
27
+ };
28
+ #hosting;
29
+ constructor(stance, hosting) {
30
+ super(stance);
31
+ this.#hosting = hosting;
32
+ }
33
+ get hosted() {
34
+ return [...this.cells.hosted];
35
+ }
36
+ async open(args) {
37
+ if (!text(args.key) || !text(args.class))
38
+ return { error: 'key and class are text' };
39
+ if (!Faculty.fulfils(this.steward.classOf(args.class)))
40
+ return { error: 'no such faculty' };
41
+ const opened = await this.stance.boot(args.class, args.key, args.key);
42
+ return opened === null ? { error: 'not opened' } : { opened };
43
+ }
44
+ async host(args) {
45
+ const name = args.ward;
46
+ if (!text(name) || name === BOX || this.hosted.includes(name))
47
+ return { error: 'no such ward to host' };
48
+ const invitation = await this.stance.occupants.invite(name, { hosted: true });
49
+ if (invitation === null)
50
+ return { error: 'not hosted' };
51
+ if (!(await this.#hosting.host(name, invitation))) {
52
+ this.stance.occupants.remove(name);
53
+ return { error: 'not hosted' };
54
+ }
55
+ this.cells.hosted.push(name);
56
+ return { hosted: name };
57
+ }
58
+ async lend(args) {
59
+ if (!text(args.contract))
60
+ return { error: 'contract is text' };
61
+ const invitation = await this.offer(args.contract);
62
+ return invitation ? { invitation } : { error: 'nothing lent' };
63
+ }
64
+ async retract(args) {
65
+ if (!text(args.contract) || !text(args.heir))
66
+ return { error: 'contract and heir are text' };
67
+ await this.withdraw(args.contract, args.heir);
68
+ return { retracted: args.heir };
69
+ }
70
+ // Lender: the first standing faculty that fulfils the contract offers.
71
+ async offer(contract) {
72
+ for (const key of this.#fulfilling(contract)) {
73
+ const out = await this.stance.standings.get(key)?.ask('offer');
74
+ const invitation = readInvitation(out?.invitation);
75
+ if (invitation)
76
+ return invitation;
77
+ }
78
+ return null;
79
+ }
80
+ async withdraw(contract, heir) {
81
+ for (const key of this.#fulfilling(contract))
82
+ await this.stance.standings.get(key)?.ask('retract', { heir });
83
+ }
84
+ #fulfilling(contract) {
85
+ const beings = this.steward.beings();
86
+ return this.stance.standings.ids().filter((key) => {
87
+ const being = beings[key];
88
+ const C = being && !being.absent ? this.steward.classOf(being.class) : undefined;
89
+ return Faculty.fulfils(C) && Faculty.contracts(C).includes(contract);
90
+ });
91
+ }
92
+ }
@@ -0,0 +1,22 @@
1
+ import type { Answer, BeingClass, Invitation, JsonObject } from '../being/index.ts';
2
+ import { Carrier } from '../contract/index.ts';
3
+ import { Ward } from '../ward/index.ts';
4
+ import { Dock, type Hosting } from './dock.ts';
5
+ import { Registry } from './registry.ts';
6
+ import type { Terrain } from './terrain.ts';
7
+ export declare class Harbor extends Carrier implements Hosting {
8
+ #private;
9
+ readonly terrain: Terrain;
10
+ readonly registry: Registry;
11
+ private constructor();
12
+ static open(terrain: Terrain, classes: readonly BeingClass[]): Promise<Harbor>;
13
+ get box(): Ward;
14
+ get dock(): Dock;
15
+ ward(name: string): Ward | undefined;
16
+ get names(): string[];
17
+ root(method?: string, args?: JsonObject): Promise<Answer>;
18
+ holds(pk: string): boolean;
19
+ arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
20
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
21
+ host(name: string, invitation: Invitation): Promise<boolean>;
22
+ }
@@ -0,0 +1,84 @@
1
+ import { Carrier } from '../contract/index.js';
2
+ import { DOCK, Ward } from '../ward/index.js';
3
+ import { BOX, Dock } from './dock.js';
4
+ import { Registry } from './registry.js';
5
+ export class Harbor extends Carrier {
6
+ terrain;
7
+ registry;
8
+ #wards = new Map();
9
+ constructor(terrain, registry) {
10
+ super();
11
+ this.terrain = terrain;
12
+ this.registry = registry;
13
+ }
14
+ static async open(terrain, classes) {
15
+ const harbor = new Harbor(terrain, new Registry(classes));
16
+ await harbor.#unpack(BOX, undefined, (stance) => new Dock(stance, harbor));
17
+ for (const name of harbor.dock.hosted)
18
+ await harbor.#unpack(name);
19
+ return harbor;
20
+ }
21
+ get box() {
22
+ return this.#wards.get(BOX);
23
+ }
24
+ get dock() {
25
+ return this.box.wardBeing;
26
+ }
27
+ ward(name) {
28
+ return this.#wards.get(name);
29
+ }
30
+ get names() {
31
+ return [...this.#wards.keys()].filter((name) => name !== BOX);
32
+ }
33
+ // The unsealed ask on the box ward: what a command line carries.
34
+ root(method, args) {
35
+ return this.box.root(method, args);
36
+ }
37
+ holds(pk) {
38
+ return this.#door(pk) !== undefined;
39
+ }
40
+ // Bytes for one of this harbor's doors, or nothing.
41
+ async arrive(pk, bytes) {
42
+ const ward = this.#door(pk);
43
+ return ward ? (await ward.door.arrive(bytes)).bytes : null;
44
+ }
45
+ // Carrier: this harbor's own doors first, then the terrain's.
46
+ carry(pk, bytes) {
47
+ return this.holds(pk) ? this.arrive(pk, bytes) : this.terrain.carrier.carry(pk, bytes);
48
+ }
49
+ // Hosting: a ward the dock hosts stands now.
50
+ async host(name, invitation) {
51
+ if (this.#wards.has(name))
52
+ return false;
53
+ try {
54
+ await this.#unpack(name, invitation);
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ if (this.#wards.get(name).wardBeing.stance.standings.get(DOCK))
60
+ return true;
61
+ this.#wards.delete(name);
62
+ return false;
63
+ }
64
+ async #unpack(name, dock, root) {
65
+ const { entropy, clock, custody, memory } = this.terrain;
66
+ const ward = await Ward.unpack({
67
+ seed: await custody.seed(name),
68
+ memory: await memory.open(name),
69
+ catalogue: this.registry,
70
+ entropy,
71
+ clock,
72
+ carrier: this,
73
+ ...(dock ? { dock } : {}),
74
+ ...(root ? { root } : {}),
75
+ });
76
+ this.#wards.set(name, ward);
77
+ }
78
+ #door(pk) {
79
+ for (const ward of this.#wards.values())
80
+ if (ward.pk === pk)
81
+ return ward;
82
+ return undefined;
83
+ }
84
+ }
@@ -1,10 +1,4 @@
1
- export { MemoryHarbor, type Booted, type WardFactory } from './memory.ts';
2
- export { Harbor, DEFAULT_CODE, type Hosted, type Bound, type Loader } from './core.ts';
3
- export { dial, type Dialer } from './dial.ts';
4
- export { MemoryStore, values, rowsOf, type Store, type Kept, type WardRecord } from './store.ts';
5
- export { HEAD, fromRows, rowsIn } from '../ward/partition.ts';
6
- export { request, Socket, SUITE, REFUSED, type Reach, type Carry, type Line, type Announce } from './reach.ts';
7
- export type { Ground, WardPointers, Memory } from '../ward/ground.ts';
8
- export { BOX, OFFER, RETRACT, volatile } from '../ward/ground.ts';
9
- export { Box, BOX_SEED, BOX_ASKS, stand, hold, offerTo, roll } from './box.ts';
10
- export { maker, entropy, learnPk } from '../ward/ground.ts';
1
+ export { BOX, Dock, type Hosting } from './dock.ts';
2
+ export { Harbor } from './harbor.ts';
3
+ export { Registry } from './registry.ts';
4
+ export { Terrain } from './terrain.ts';
@@ -1,19 +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 } from './memory.js';
7
- export { Harbor, DEFAULT_CODE } from './core.js';
8
- export { dial } from './dial.js';
9
- export { MemoryStore, values, rowsOf } from './store.js';
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.js';
13
- export { request, Socket, SUITE, REFUSED } from './reach.js';
14
- export { BOX, OFFER, RETRACT, volatile } from '../ward/ground.js';
15
- // The box as one being, and the acts a harbor runs to stand it.
16
- export { Box, BOX_SEED, BOX_ASKS, stand, hold, offerTo, roll } from './box.js';
17
- // What a harbor builds a ground out of. Convenience, never contract: a kit
18
- // writing its own harbor may write these three again.
19
- export { maker, entropy, learnPk } from '../ward/ground.js';
2
+ // The onion: a terrain, a registry, the dock, and the harbor.
3
+ export { BOX, Dock } from './dock.js';
4
+ export { Harbor } from './harbor.js';
5
+ export { Registry } from './registry.js';
6
+ export { Terrain } from './terrain.js';
@@ -0,0 +1,7 @@
1
+ import type { BeingClass } from '../being/index.ts';
2
+ import type { Catalogue } from '../ward/index.ts';
3
+ export declare class Registry implements Catalogue {
4
+ #private;
5
+ constructor(classes: readonly BeingClass[]);
6
+ classOf(name: string): BeingClass | undefined;
7
+ }
@@ -0,0 +1,13 @@
1
+ export class Registry {
2
+ #classes = new Map();
3
+ constructor(classes) {
4
+ for (const C of classes) {
5
+ if (this.#classes.has(C.name) && this.#classes.get(C.name) !== C)
6
+ throw new Error(`two classes are named ${C.name}`);
7
+ this.#classes.set(C.name, C);
8
+ }
9
+ }
10
+ classOf(name) {
11
+ return this.#classes.get(name);
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ import type { Carrier, Clock, Custody, Entropy, Memory } from '../contract/index.ts';
2
+ export declare abstract class Terrain {
3
+ abstract readonly entropy: Entropy;
4
+ abstract readonly clock: Clock;
5
+ abstract readonly custody: Custody;
6
+ abstract readonly memory: Memory;
7
+ abstract readonly carrier: Carrier;
8
+ }
@@ -0,0 +1,2 @@
1
+ export class Terrain {
2
+ }
@@ -0,0 +1,7 @@
1
+ export { Being, canonical, digest, Faculty, isSilence, isWord, OWNER, silence, told, WARD, word } from './being/index.ts';
2
+ 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';
3
+ export { readInvitation } from './quo/index.ts';
4
+ export { Carrier, Clock, Custody, Entropy, Memory, type Rows } from './contract/index.ts';
5
+ export { CEILING, DEFAULT, DEPTH, ownerAsk, pilots, WardBeing, type Steward, type WardBeingStance } from './ward/index.ts';
6
+ export { BOX, Dock, Harbor, Registry, Terrain } from './harbor/index.ts';
7
+ export { CryptoEntropy, HeldCustody, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World, type PointerParts } from './pointer/index.ts';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
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
+ // Beings, faculties, and what they are told.
5
+ export { Being, canonical, digest, Faculty, isSilence, isWord, OWNER, silence, told, WARD, word } from './being/index.js';
6
+ export { readInvitation } from './quo/index.js';
7
+ // What a terrain gives a harbor.
8
+ export { Carrier, Clock, Custody, Entropy, Memory } from './contract/index.js';
9
+ // Wards and the ward-being.
10
+ export { CEILING, DEFAULT, DEPTH, ownerAsk, pilots, WardBeing } from './ward/index.js';
11
+ // The onion.
12
+ export { BOX, Dock, Harbor, Registry, Terrain } from './harbor/index.js';
13
+ // The pointer dock: a whole world in one process.
14
+ export { CryptoEntropy, HeldCustody, NoCarrier, PointerCarrier, PointerTerrain, SystemClock, VolatileMemory, World } from './pointer/index.js';
@@ -0,0 +1,20 @@
1
+ import { Clock, Custody, Entropy, Memory, type Rows } from '../contract/index.ts';
2
+ export declare class CryptoEntropy extends Entropy {
3
+ draw(length: number): Uint8Array;
4
+ }
5
+ export declare class SystemClock extends Clock {
6
+ now(): number;
7
+ wait(ms: number): {
8
+ done: Promise<void>;
9
+ cancel(): void;
10
+ };
11
+ }
12
+ export declare class HeldCustody extends Custody {
13
+ #private;
14
+ constructor(entropy: Entropy);
15
+ seed(name: string): Promise<Uint8Array>;
16
+ }
17
+ export declare class VolatileMemory extends Memory {
18
+ #private;
19
+ open(name: string): Promise<Rows>;
20
+ }
@@ -0,0 +1,45 @@
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 } from '../contract/index.js';
5
+ export class CryptoEntropy extends Entropy {
6
+ draw(length) {
7
+ return globalThis.crypto.getRandomValues(new Uint8Array(length));
8
+ }
9
+ }
10
+ export class SystemClock extends Clock {
11
+ now() {
12
+ return Date.now();
13
+ }
14
+ wait(ms) {
15
+ let timer;
16
+ const done = new Promise((resolve) => (timer = setTimeout(resolve, ms)));
17
+ return { done, cancel: () => clearTimeout(timer) };
18
+ }
19
+ }
20
+ // Seeds drawn once per name and held by this object.
21
+ export class HeldCustody extends Custody {
22
+ #entropy;
23
+ #seeds = new Map();
24
+ constructor(entropy) {
25
+ super();
26
+ this.#entropy = entropy;
27
+ }
28
+ seed(name) {
29
+ let seed = this.#seeds.get(name);
30
+ if (!seed)
31
+ this.#seeds.set(name, (seed = this.#entropy.draw(32)));
32
+ return Promise.resolve(seed);
33
+ }
34
+ }
35
+ // Rows that live as long as this object: opening a name again gives the
36
+ // same rows, which is what a restart in one process finds.
37
+ export class VolatileMemory extends Memory {
38
+ #wards = new Map();
39
+ open(name) {
40
+ let rows = this.#wards.get(name);
41
+ if (!rows)
42
+ this.#wards.set(name, (rows = { rows: {}, told: () => { }, kept: () => Promise.resolve(true) }));
43
+ return Promise.resolve(rows);
44
+ }
45
+ }
@@ -0,0 +1,2 @@
1
+ export { CryptoEntropy, HeldCustody, SystemClock, VolatileMemory } from './bodies.ts';
2
+ export { NoCarrier, PointerCarrier, PointerTerrain, World, type PointerParts } from './world.ts';
@@ -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.js';
4
+ export { NoCarrier, PointerCarrier, PointerTerrain, World } from './world.js';
@@ -0,0 +1,34 @@
1
+ import type { BeingClass } from '../being/index.ts';
2
+ import { Carrier, type Clock, type Custody, type Entropy, type Memory } from '../contract/index.ts';
3
+ import { Harbor, Terrain } from '../harbor/index.ts';
4
+ export declare class NoCarrier extends Carrier {
5
+ carry(): Promise<Uint8Array | null>;
6
+ }
7
+ export declare class PointerCarrier extends Carrier {
8
+ #private;
9
+ constructor(world: World);
10
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
11
+ }
12
+ export type PointerParts = {
13
+ entropy?: Entropy;
14
+ clock?: Clock;
15
+ custody?: Custody;
16
+ memory?: Memory;
17
+ carrier?: Carrier;
18
+ };
19
+ export declare class PointerTerrain extends Terrain {
20
+ readonly entropy: Entropy;
21
+ readonly clock: Clock;
22
+ readonly custody: Custody;
23
+ readonly memory: Memory;
24
+ readonly carrier: Carrier;
25
+ constructor(parts?: PointerParts);
26
+ }
27
+ export declare class World {
28
+ #private;
29
+ readonly cut: Set<string>;
30
+ harbor(name: string, classes: readonly BeingClass[], parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor>;
31
+ get(name: string): Harbor;
32
+ restart(name: string, classes?: readonly BeingClass[], parts?: Omit<PointerParts, 'carrier'>): Promise<Harbor>;
33
+ carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null>;
34
+ }