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.
- package/README.md +156 -104
- package/dist/being/being.d.ts +8 -15
- package/dist/being/being.js +20 -67
- package/dist/being/digest.d.ts +1 -1
- package/dist/being/digest.js +7 -27
- package/dist/being/faculty.d.ts +10 -0
- package/dist/being/faculty.js +74 -0
- package/dist/being/index.d.ts +5 -5
- package/dist/being/index.js +6 -5
- package/dist/being/kind.d.ts +4 -0
- package/dist/being/kind.js +28 -0
- package/dist/being/types.d.ts +35 -63
- package/dist/being/types.js +3 -57
- package/dist/being/words.d.ts +13 -0
- package/dist/being/words.js +14 -0
- package/dist/cli/command.d.ts +13 -0
- package/dist/cli/command.js +194 -0
- package/dist/cli/harbor.d.ts +21 -0
- package/dist/cli/harbor.js +141 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +14 -0
- package/dist/contract/index.d.ts +39 -0
- package/dist/contract/index.js +33 -0
- package/dist/crypto/aes.d.ts +3 -0
- package/dist/crypto/aes.js +24 -0
- package/dist/crypto/bytes.d.ts +6 -0
- package/dist/crypto/bytes.js +33 -0
- package/dist/crypto/ed25519.d.ts +3 -0
- package/dist/crypto/ed25519.js +85 -0
- package/dist/crypto/hash.d.ts +2 -0
- package/dist/crypto/hash.js +11 -0
- package/dist/crypto/index.d.ts +7 -0
- package/dist/crypto/index.js +10 -0
- package/dist/crypto/json.d.ts +8 -0
- package/dist/crypto/json.js +250 -0
- package/dist/crypto/mlkem.d.ts +12 -0
- package/dist/crypto/mlkem.js +36 -0
- package/dist/crypto/subtle.d.ts +10 -0
- package/dist/crypto/subtle.js +29 -0
- package/dist/crypto/x25519.d.ts +2 -0
- package/dist/crypto/x25519.js +21 -0
- package/dist/folder/index.d.ts +14 -0
- package/dist/folder/index.js +87 -0
- package/dist/harbor/catalogue.d.ts +25 -0
- package/dist/harbor/catalogue.js +93 -0
- package/dist/harbor/dock.d.ts +29 -0
- package/dist/harbor/dock.js +122 -0
- package/dist/harbor/harbor.d.ts +31 -0
- package/dist/harbor/harbor.js +146 -0
- package/dist/harbor/index.d.ts +7 -10
- package/dist/harbor/index.js +9 -18
- package/dist/harbor/package.d.ts +10 -0
- package/dist/harbor/package.js +115 -0
- package/dist/harbor/registry.d.ts +9 -0
- package/dist/harbor/registry.js +78 -0
- package/dist/harbor/root-line.d.ts +9 -0
- package/dist/harbor/root-line.js +36 -0
- package/dist/harbor/terrain.d.ts +9 -0
- package/dist/harbor/terrain.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/pointer/bodies.d.ts +26 -0
- package/dist/pointer/bodies.js +64 -0
- package/dist/pointer/index.d.ts +2 -0
- package/dist/pointer/index.js +4 -0
- package/dist/pointer/world.d.ts +35 -0
- package/dist/pointer/world.js +85 -0
- package/dist/quo/door.d.ts +34 -0
- package/dist/quo/door.js +172 -0
- package/dist/quo/index.d.ts +8 -0
- package/dist/quo/index.js +11 -0
- package/dist/quo/invitation.d.ts +7 -0
- package/dist/quo/invitation.js +15 -0
- package/dist/quo/keys.d.ts +40 -0
- package/dist/quo/keys.js +79 -0
- package/dist/quo/payload.d.ts +15 -0
- package/dist/quo/payload.js +55 -0
- package/dist/quo/relations.d.ts +40 -0
- package/dist/quo/relations.js +33 -0
- package/dist/quo/reply.d.ts +13 -0
- package/dist/quo/reply.js +35 -0
- package/dist/quo/seal.d.ts +42 -0
- package/dist/quo/seal.js +78 -0
- package/dist/quo/standing.d.ts +39 -0
- package/dist/quo/standing.js +90 -0
- package/dist/tcp/frame.d.ts +29 -0
- package/dist/tcp/frame.js +74 -0
- package/dist/tcp/index.d.ts +18 -0
- package/dist/tcp/index.js +164 -0
- package/dist/ward/allowance.d.ts +13 -9
- package/dist/ward/allowance.js +28 -67
- package/dist/ward/cells.d.ts +6 -6
- package/dist/ward/cells.js +67 -179
- package/dist/ward/index.d.ts +6 -10
- package/dist/ward/index.js +7 -13
- package/dist/ward/partition.d.ts +44 -60
- package/dist/ward/partition.js +56 -285
- package/dist/ward/pilot.d.ts +8 -0
- package/dist/ward/pilot.js +50 -0
- package/dist/ward/stance.d.ts +27 -20
- package/dist/ward/stance.js +174 -406
- package/dist/ward/ward-being.d.ts +35 -0
- package/dist/ward/ward-being.js +82 -0
- package/dist/ward/ward.d.ts +45 -11
- package/dist/ward/ward.js +239 -357
- package/package.json +24 -31
- package/src/being/being.ts +35 -78
- package/src/being/digest.ts +7 -35
- package/src/being/faculty.ts +73 -0
- package/src/being/index.ts +6 -6
- package/src/being/kind.ts +30 -0
- package/src/being/types.ts +50 -146
- package/src/being/words.ts +31 -0
- package/src/cli/command.ts +188 -0
- package/src/cli/harbor.ts +137 -0
- package/src/cli/main.ts +13 -0
- package/src/contract/index.ts +73 -0
- package/src/crypto/aes.ts +26 -0
- package/src/crypto/bytes.ts +37 -0
- package/src/crypto/ed25519.ts +84 -0
- package/src/crypto/hash.ts +14 -0
- package/src/crypto/index.ts +10 -0
- package/src/crypto/json.ts +241 -0
- package/src/crypto/mlkem.ts +38 -0
- package/src/crypto/subtle.ts +33 -0
- package/src/crypto/x25519.ts +21 -0
- package/src/folder/index.ts +92 -0
- package/src/harbor/catalogue.ts +110 -0
- package/src/harbor/dock.ts +137 -0
- package/src/harbor/harbor.ts +180 -0
- package/src/harbor/index.ts +9 -19
- package/src/harbor/package.ts +131 -0
- package/src/harbor/registry.ts +73 -0
- package/src/harbor/root-line.ts +36 -0
- package/src/harbor/terrain.ts +15 -0
- package/src/index.ts +20 -0
- package/src/pointer/bodies.ts +65 -0
- package/src/pointer/index.ts +4 -0
- package/src/pointer/world.ts +94 -0
- package/src/quo/door.ts +178 -0
- package/src/quo/index.ts +11 -0
- package/src/quo/invitation.ts +15 -0
- package/src/quo/keys.ts +91 -0
- package/src/quo/payload.ts +61 -0
- package/src/quo/relations.ts +62 -0
- package/src/quo/reply.ts +38 -0
- package/src/quo/seal.ts +101 -0
- package/src/quo/standing.ts +111 -0
- package/src/stand/main.ts +20 -0
- package/src/stand/stand.ts +220 -0
- package/src/tcp/frame.ts +78 -0
- package/src/tcp/index.ts +173 -0
- package/src/ward/allowance.ts +37 -75
- package/src/ward/cells.ts +63 -176
- package/src/ward/index.ts +7 -17
- package/src/ward/partition.ts +86 -326
- package/src/ward/pilot.ts +50 -0
- package/src/ward/stance.ts +186 -420
- package/src/ward/ward-being.ts +103 -0
- package/src/ward/ward.ts +256 -359
- package/dist/being/lent.d.ts +0 -32
- package/dist/being/lent.js +0 -72
- package/dist/being/silence.d.ts +0 -12
- package/dist/being/silence.js +0 -41
- package/dist/conformance/assert.d.ts +0 -11
- package/dist/conformance/assert.js +0 -106
- package/dist/conformance/beings.d.ts +0 -199
- package/dist/conformance/beings.js +0 -188
- package/dist/conformance/estate.d.ts +0 -5
- package/dist/conformance/estate.js +0 -388
- package/dist/conformance/index.d.ts +0 -80
- package/dist/conformance/index.js +0 -819
- package/dist/conformance/reach.d.ts +0 -10
- package/dist/conformance/reach.js +0 -72
- package/dist/conformance/store.d.ts +0 -5
- package/dist/conformance/store.js +0 -113
- package/dist/harbor/box.d.ts +0 -121
- package/dist/harbor/box.js +0 -121
- package/dist/harbor/core.d.ts +0 -55
- package/dist/harbor/core.js +0 -662
- package/dist/harbor/dial.d.ts +0 -9
- package/dist/harbor/dial.js +0 -81
- package/dist/harbor/memory.d.ts +0 -29
- package/dist/harbor/memory.js +0 -104
- package/dist/harbor/reach.d.ts +0 -36
- package/dist/harbor/reach.js +0 -199
- package/dist/harbor/store.d.ts +0 -35
- package/dist/harbor/store.js +0 -62
- package/dist/vector/cases.d.ts +0 -42
- package/dist/vector/cases.js +0 -223
- package/dist/vector/index.d.ts +0 -6
- package/dist/vector/index.js +0 -8
- package/dist/vector/stand.d.ts +0 -9
- package/dist/vector/stand.js +0 -77
- package/dist/vector/world.d.ts +0 -144
- package/dist/vector/world.js +0 -209
- package/dist/ward/arithmetic.d.ts +0 -31
- package/dist/ward/arithmetic.js +0 -249
- package/dist/ward/door.d.ts +0 -18
- package/dist/ward/door.js +0 -186
- package/dist/ward/ground.d.ts +0 -29
- package/dist/ward/ground.js +0 -56
- package/dist/ward/heirs.d.ts +0 -13
- package/dist/ward/heirs.js +0 -117
- package/dist/ward/json.d.ts +0 -2
- package/dist/ward/json.js +0 -163
- package/dist/ward/owner.d.ts +0 -13
- package/dist/ward/owner.js +0 -220
- package/dist/ward/seal.d.ts +0 -52
- package/dist/ward/seal.js +0 -150
- package/src/being/lent.ts +0 -72
- package/src/being/silence.ts +0 -46
- package/src/conformance/assert.ts +0 -100
- package/src/conformance/beings.ts +0 -188
- package/src/conformance/estate.ts +0 -412
- package/src/conformance/index.ts +0 -965
- package/src/conformance/reach.ts +0 -83
- package/src/conformance/store.ts +0 -125
- package/src/harbor/box.ts +0 -131
- package/src/harbor/core.ts +0 -699
- package/src/harbor/dial.ts +0 -112
- package/src/harbor/memory.ts +0 -123
- package/src/harbor/reach.ts +0 -221
- package/src/harbor/store.ts +0 -91
- package/src/vector/cases.ts +0 -257
- package/src/vector/index.ts +0 -11
- package/src/vector/stand.ts +0 -76
- package/src/vector/world.ts +0 -232
- package/src/ward/arithmetic.ts +0 -251
- package/src/ward/door.ts +0 -186
- package/src/ward/ground.ts +0 -142
- package/src/ward/heirs.ts +0 -116
- package/src/ward/json.ts +0 -144
- package/src/ward/owner.ts +0 -214
- package/src/ward/seal.ts +0 -178
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The catalogue: the dock's first faculty, the code a harbor runs. Her
|
|
3
|
+
// cells name each module by the name it declares, its version, and where
|
|
4
|
+
// the terrain's loader finds it. The harbor wakes her before any other
|
|
5
|
+
// faculty, loads every module she names, and refuses to stand when one is
|
|
6
|
+
// missing.
|
|
7
|
+
//
|
|
8
|
+
// add { from } the module the loader finds there, run from now
|
|
9
|
+
// on; the same module again takes its new version
|
|
10
|
+
// remove { module } a module no being stands on, run no more
|
|
11
|
+
// modules what she names, each with the version running
|
|
12
|
+
//
|
|
13
|
+
// She is the kit's, of kind `org.nervur.catalogue`, and answers the root
|
|
14
|
+
// and an owner alone.
|
|
15
|
+
import { Faculty, type AskSpec, type BeingClass, type JsonObject, type Stance } from '../being/index.ts';
|
|
16
|
+
import type { Loaded } from '../contract/index.ts';
|
|
17
|
+
import { ownerAsk } from '../ward/index.ts';
|
|
18
|
+
|
|
19
|
+
export const CATALOGUE = 'catalogue';
|
|
20
|
+
|
|
21
|
+
// One module, as her cells keep it.
|
|
22
|
+
export type ModuleRow = { version: string; from: string };
|
|
23
|
+
|
|
24
|
+
// What the catalogue asks of her harbor.
|
|
25
|
+
export interface Cataloguing {
|
|
26
|
+
load(from: string): Promise<Loaded | null>;
|
|
27
|
+
// The modules run from now on, or why they cannot be.
|
|
28
|
+
run(modules: readonly Loaded[]): string | null;
|
|
29
|
+
// Whether a being of any ward stands on one of these kinds.
|
|
30
|
+
stands(kinds: readonly string[]): boolean;
|
|
31
|
+
kindsOf(module: string): string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const text = (v: unknown): v is string => typeof v === 'string';
|
|
35
|
+
|
|
36
|
+
export class Catalogue extends Faculty {
|
|
37
|
+
static override readonly kind: string = 'org.nervur.catalogue';
|
|
38
|
+
static override cells: JsonObject = { modules: {} };
|
|
39
|
+
static override asks: Record<string, AskSpec> = {
|
|
40
|
+
add: ownerAsk('a module run from now on', { required: ['from'] }),
|
|
41
|
+
remove: ownerAsk('a module run no more', { required: ['module'] }),
|
|
42
|
+
modules: ownerAsk('the modules this harbor runs'),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
readonly #harbor: Cataloguing;
|
|
46
|
+
readonly #running = new Map<string, Loaded>();
|
|
47
|
+
|
|
48
|
+
constructor(stance: Stance, harbor: Cataloguing) {
|
|
49
|
+
super(stance);
|
|
50
|
+
this.#harbor = harbor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get #rows(): Record<string, ModuleRow> {
|
|
54
|
+
return this.cells.modules as Record<string, ModuleRow>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Every module her cells name, loaded and run, or a throw naming the one
|
|
58
|
+
// that cannot be.
|
|
59
|
+
async wake(): Promise<void> {
|
|
60
|
+
for (const [module, row] of Object.entries(this.#rows)) {
|
|
61
|
+
const loaded = await this.#harbor.load(row.from);
|
|
62
|
+
if (loaded === null) throw new Error(`module ${module} is not found at ${row.from}`);
|
|
63
|
+
if (loaded.module !== module) throw new Error(`${row.from} is module ${loaded.module}, not ${module}`);
|
|
64
|
+
this.#running.set(module, loaded);
|
|
65
|
+
}
|
|
66
|
+
const refused = this.#harbor.run([...this.#running.values()]);
|
|
67
|
+
if (refused !== null) throw new Error(refused);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async add(args: JsonObject): Promise<JsonObject> {
|
|
71
|
+
if (!text(args.from)) return { error: 'from is text' };
|
|
72
|
+
const loaded = await this.#harbor.load(args.from);
|
|
73
|
+
if (loaded === null) return { error: 'no such module' };
|
|
74
|
+
const running = new Map(this.#running);
|
|
75
|
+
running.set(loaded.module, loaded);
|
|
76
|
+
const refused = this.#harbor.run([...running.values()]);
|
|
77
|
+
if (refused !== null) return { error: refused };
|
|
78
|
+
this.#running.set(loaded.module, loaded);
|
|
79
|
+
this.#rows[loaded.module] = { version: loaded.version, from: loaded.from };
|
|
80
|
+
return { added: loaded.module, version: loaded.version };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
remove(args: JsonObject): JsonObject {
|
|
84
|
+
const module = args.module;
|
|
85
|
+
if (!text(module) || !this.#running.has(module)) return { error: 'no such module' };
|
|
86
|
+
if (this.#harbor.stands(this.#harbor.kindsOf(module))) return { error: 'a being stands on this module' };
|
|
87
|
+
const running = new Map(this.#running);
|
|
88
|
+
running.delete(module);
|
|
89
|
+
this.#harbor.run([...running.values()]);
|
|
90
|
+
this.#running.delete(module);
|
|
91
|
+
delete this.#rows[module];
|
|
92
|
+
return { removed: module };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
modules(): JsonObject {
|
|
96
|
+
const out: JsonObject = {};
|
|
97
|
+
for (const [module, row] of Object.entries(this.#rows)) out[module] = { version: row.version, running: this.#running.get(module)?.version ?? null, from: row.from };
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The catalogue class one harbor stands: a being is born of her class and
|
|
103
|
+
// her stance alone, so the class holds its harbor.
|
|
104
|
+
export const catalogueFor = (harbor: Cataloguing): BeingClass =>
|
|
105
|
+
class extends Catalogue {
|
|
106
|
+
static override readonly kind: string = Catalogue.kind;
|
|
107
|
+
constructor(stance: Stance) {
|
|
108
|
+
super(stance, harbor);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
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, seed? } a hosted ward stands under the seed named,
|
|
9
|
+
// or a drawn one, holding a standing on the
|
|
10
|
+
// dock; the dock keeps its name and seed
|
|
11
|
+
// lend { contract } asked by a hosted ward: an offer from a
|
|
12
|
+
// faculty that fulfils the contract
|
|
13
|
+
// retract { contract, heir } an offer that was not taken
|
|
14
|
+
// route { ward, at } where a ward pk is reached, `host:port`, or
|
|
15
|
+
// null to forget it; the dock keeps it and
|
|
16
|
+
// the harbor hands it to its carrier
|
|
17
|
+
import { Faculty, KIT, type Asker, type AskSpec, type Invitation, type JsonObject } from '../being/index.ts';
|
|
18
|
+
import { CATALOGUE } from './catalogue.ts';
|
|
19
|
+
import { readInvitation } from '../quo/index.ts';
|
|
20
|
+
import { ownerAsk, WardBeing, type Lender, type WardBeingStance } from '../ward/index.ts';
|
|
21
|
+
|
|
22
|
+
// What the dock asks of her harbor: a ward standing, and the seed, as hex,
|
|
23
|
+
// it stands under.
|
|
24
|
+
export interface Hosting {
|
|
25
|
+
host(name: string, invitation: Invitation, seed?: string): Promise<string | null>;
|
|
26
|
+
// Whether the harbor's carrier took the route.
|
|
27
|
+
route(pk: string, at: string | null): boolean;
|
|
28
|
+
// Whether the box ward's partition was kept.
|
|
29
|
+
keep(): Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// One hosted ward, as the dock's cells keep it.
|
|
33
|
+
export type Hosted = { ward: string; seed: string };
|
|
34
|
+
|
|
35
|
+
export const BOX = 'box';
|
|
36
|
+
const text = (v: unknown): v is string => typeof v === 'string';
|
|
37
|
+
const hosted: AskSpec['for'] = (_: Asker, notes: JsonObject | undefined) => notes?.hosted === true;
|
|
38
|
+
|
|
39
|
+
export class Dock extends WardBeing implements Lender {
|
|
40
|
+
static override readonly kind: string = 'org.nervur.dock';
|
|
41
|
+
static override cells: JsonObject = { hosted: [], routes: {} };
|
|
42
|
+
static override asks: Record<string, AskSpec> = {
|
|
43
|
+
...WardBeing.asks,
|
|
44
|
+
open: ownerAsk('a faculty of this harbor', { required: ['key', 'class'] }),
|
|
45
|
+
host: ownerAsk('a ward this harbor hosts', { required: ['ward'] }),
|
|
46
|
+
route: ownerAsk('where a ward is reached, or null', { required: ['ward', 'at'] }),
|
|
47
|
+
lend: { description: 'an offer from a faculty fulfilling a contract', input: { type: 'object', required: ['contract'] }, for: hosted },
|
|
48
|
+
retract: { description: 'an offer that was not taken', input: { type: 'object', required: ['contract', 'heir'] }, for: hosted },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
readonly #hosting: Hosting;
|
|
52
|
+
|
|
53
|
+
constructor(stance: WardBeingStance, hosting: Hosting) {
|
|
54
|
+
super(stance);
|
|
55
|
+
this.#hosting = hosting;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get hosted(): Hosted[] {
|
|
59
|
+
return (this.cells.hosted as Hosted[]).map(({ ward, seed }) => ({ ward, seed }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get routes(): Record<string, string> {
|
|
63
|
+
return { ...(this.cells.routes as Record<string, string>) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
route(args: JsonObject): JsonObject {
|
|
67
|
+
const { ward, at } = args;
|
|
68
|
+
if (!text(ward) || (at !== null && !text(at)) || !this.#hosting.route(ward, at)) return { error: 'not routed' };
|
|
69
|
+
const routes = this.cells.routes as Record<string, string>;
|
|
70
|
+
if (at === null) delete routes[ward];
|
|
71
|
+
else routes[ward] = at;
|
|
72
|
+
return { routed: ward };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Genesis: the catalogue stands first, and is kept at once.
|
|
76
|
+
async standCatalogue(): Promise<void> {
|
|
77
|
+
const kind = 'org.nervur.catalogue';
|
|
78
|
+
if ((await this.stance.boot(kind, CATALOGUE, CATALOGUE)) === null || !(await this.#hosting.keep())) throw new Error('the catalogue did not stand');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async open(args: JsonObject): Promise<JsonObject> {
|
|
82
|
+
if (!text(args.key) || !text(args.class)) return { error: 'key and class are text' };
|
|
83
|
+
if (args.class.startsWith(KIT) || !Faculty.fulfils(this.steward.classOf(args.class))) return { error: 'no such faculty' };
|
|
84
|
+
const opened = await this.stance.boot(args.class, args.key, args.key);
|
|
85
|
+
return opened === null ? { error: 'not opened' } : { opened };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async host(args: JsonObject): Promise<JsonObject> {
|
|
89
|
+
const name = args.ward;
|
|
90
|
+
if (!text(name) || name === BOX || this.hosted.some((h) => h.ward === name)) return { error: 'no such ward to host' };
|
|
91
|
+
if (args.seed !== undefined && !text(args.seed)) return { error: 'seed is text' };
|
|
92
|
+
const invitation = await this.stance.occupants.invite(name, { hosted: true });
|
|
93
|
+
if (invitation === null) return { error: 'not hosted' };
|
|
94
|
+
const seed = await this.#hosting.host(name, invitation, args.seed);
|
|
95
|
+
if (seed === null) {
|
|
96
|
+
this.stance.occupants.remove(name);
|
|
97
|
+
return { error: 'not hosted' };
|
|
98
|
+
}
|
|
99
|
+
(this.cells.hosted as Hosted[]).push({ ward: name, seed });
|
|
100
|
+
return { hosted: name };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async lend(args: JsonObject): Promise<JsonObject> {
|
|
104
|
+
if (!text(args.contract)) return { error: 'contract is text' };
|
|
105
|
+
const invitation = await this.offer(args.contract);
|
|
106
|
+
return invitation ? { invitation } : { error: 'nothing lent' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async retract(args: JsonObject): Promise<JsonObject> {
|
|
110
|
+
if (!text(args.contract) || !text(args.heir)) return { error: 'contract and heir are text' };
|
|
111
|
+
await this.withdraw(args.contract, args.heir);
|
|
112
|
+
return { retracted: args.heir };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Lender: the first standing faculty that fulfils the contract offers.
|
|
116
|
+
async offer(contract: string): Promise<Invitation | null> {
|
|
117
|
+
for (const key of this.#fulfilling(contract)) {
|
|
118
|
+
const out = await this.stance.standings.get(key)?.ask('offer');
|
|
119
|
+
const invitation = readInvitation((out as { invitation?: unknown } | undefined)?.invitation);
|
|
120
|
+
if (invitation) return invitation;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async withdraw(contract: string, heir: string): Promise<void> {
|
|
126
|
+
for (const key of this.#fulfilling(contract)) await this.stance.standings.get(key)?.ask('retract', { heir });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#fulfilling(contract: string): string[] {
|
|
130
|
+
const beings = this.steward.beings();
|
|
131
|
+
return this.stance.standings.ids().filter((key) => {
|
|
132
|
+
const being = beings[key];
|
|
133
|
+
const C = being && !being.absent ? this.steward.classOf(being.class) : undefined;
|
|
134
|
+
return Faculty.fulfils(C) && Faculty.contracts(C).includes(contract);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The harbor: the onion, unpacked from a terrain alone.
|
|
3
|
+
//
|
|
4
|
+
// 1. the seed and the package the harbor's one secret, from custody,
|
|
5
|
+
// opens its sealed memory
|
|
6
|
+
// 2. the box ward unpacked as every ward is, under the
|
|
7
|
+
// harbor's seed, its ward-being the dock
|
|
8
|
+
// 3. the catalogue the dock's first faculty, a kit class:
|
|
9
|
+
// every module her cells name is loaded
|
|
10
|
+
// through the terrain, or the harbor does
|
|
11
|
+
// not stand
|
|
12
|
+
// 4. the faculties the box ward's other beings, each from her
|
|
13
|
+
// own row, by the kinds the modules bring
|
|
14
|
+
// 5. the routes as the dock's cells keep them, handed to
|
|
15
|
+
// the terrain's carrier
|
|
16
|
+
// 6. the hosted wards as the dock's cells list them, each under
|
|
17
|
+
// the seed the dock keeps for it
|
|
18
|
+
// 7. the beings in each ward, each from her own row
|
|
19
|
+
//
|
|
20
|
+
// Genesis and wake are one path: empty memory unpacks a box ward holding
|
|
21
|
+
// the dock, the dock stands the catalogue, and the root's asks write the
|
|
22
|
+
// rest.
|
|
23
|
+
import type { Answer, BeingClass, Invitation, JsonObject } from '../being/index.ts';
|
|
24
|
+
import { Carrier, type Loaded } from '../contract/index.ts';
|
|
25
|
+
import { hex, isHex, unhex } from '../crypto/index.ts';
|
|
26
|
+
import { WardKey } from '../quo/index.ts';
|
|
27
|
+
import { DOCK, Pilot, Ward, type Catalogue as Resolving, type WardParts } from '../ward/index.ts';
|
|
28
|
+
import { Catalogue, CATALOGUE, catalogueFor, type Cataloguing } from './catalogue.ts';
|
|
29
|
+
import { BOX, Dock, type Hosting } from './dock.ts';
|
|
30
|
+
import { Package } from './package.ts';
|
|
31
|
+
import { Registry } from './registry.ts';
|
|
32
|
+
import type { Terrain } from './terrain.ts';
|
|
33
|
+
|
|
34
|
+
export class Harbor extends Carrier implements Hosting, Cataloguing, Resolving {
|
|
35
|
+
readonly terrain: Terrain;
|
|
36
|
+
readonly #package: Package;
|
|
37
|
+
readonly #wards = new Map<string, Ward>();
|
|
38
|
+
readonly #kit: BeingClass[];
|
|
39
|
+
#registry: Registry;
|
|
40
|
+
|
|
41
|
+
private constructor(terrain: Terrain, pack: Package) {
|
|
42
|
+
super();
|
|
43
|
+
this.terrain = terrain;
|
|
44
|
+
this.#package = pack;
|
|
45
|
+
this.#kit = [catalogueFor(this), Pilot];
|
|
46
|
+
this.#registry = new Registry(this.#kit, []);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static async open(terrain: Terrain): Promise<Harbor> {
|
|
50
|
+
const seed = await terrain.custody.seed();
|
|
51
|
+
const harbor = new Harbor(terrain, await Package.open(terrain.memory, terrain.entropy, seed));
|
|
52
|
+
await harbor.#unpack(BOX, seed, undefined, (stance) => new Dock(stance, harbor));
|
|
53
|
+
if (!harbor.box.partition.being(CATALOGUE)) await harbor.dock.standCatalogue();
|
|
54
|
+
await harbor.catalogue.wake();
|
|
55
|
+
for (const [pk, at] of Object.entries(harbor.dock.routes)) terrain.carrier.route(pk, at);
|
|
56
|
+
for (const hosted of harbor.dock.hosted) await harbor.#unpack(hosted.ward, unhex(hosted.seed));
|
|
57
|
+
return harbor;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get box(): Ward {
|
|
61
|
+
return this.#wards.get(BOX)!;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get dock(): Dock {
|
|
65
|
+
return this.box.wardBeing as Dock;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get catalogue(): Catalogue {
|
|
69
|
+
return this.box.being(CATALOGUE) as Catalogue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get registry(): Registry {
|
|
73
|
+
return this.#registry;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ward(name: string): Ward | undefined {
|
|
77
|
+
return this.#wards.get(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get names(): string[] {
|
|
81
|
+
return [...this.#wards.keys()].filter((name) => name !== BOX);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The unsealed ask on the box ward: what a command line carries.
|
|
85
|
+
root(method?: string, args?: JsonObject): Promise<Answer> {
|
|
86
|
+
return this.box.root(method, args);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
holds(pk: string): boolean {
|
|
90
|
+
return this.#door(pk) !== undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Bytes for one of this harbor's doors, or nothing.
|
|
94
|
+
async arrive(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
|
|
95
|
+
const ward = this.#door(pk);
|
|
96
|
+
return ward ? (await ward.door.arrive(bytes)).bytes : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Carrier: this harbor's own doors first, then the terrain's.
|
|
100
|
+
carry(pk: string, bytes: Uint8Array): Promise<Uint8Array | null> {
|
|
101
|
+
return this.holds(pk) ? this.arrive(pk, bytes) : this.terrain.carrier.carry(pk, bytes);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Resolving: a kind, by the registry the catalogue runs.
|
|
105
|
+
classOf(kind: string): BeingClass | undefined {
|
|
106
|
+
return this.#registry.classOf(kind);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Cataloguing: code through the terrain, and the modules run.
|
|
110
|
+
load(from: string): Promise<Loaded | null> {
|
|
111
|
+
return this.terrain.loader.load(from);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
run(modules: readonly Loaded[]): string | null {
|
|
115
|
+
try {
|
|
116
|
+
this.#registry = new Registry(this.#kit, modules);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return (e as Error).message;
|
|
119
|
+
}
|
|
120
|
+
for (const ward of this.#wards.values()) ward.bearAnew();
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
stands(kinds: readonly string[]): boolean {
|
|
125
|
+
for (const ward of this.#wards.values()) {
|
|
126
|
+
for (const row of Object.values(ward.partition.beings)) if (kinds.includes(row.class)) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
kindsOf(module: string): string[] {
|
|
132
|
+
return this.#registry.kindsOf(module);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Hosting: a ward the dock hosts stands now, under the seed named or a
|
|
136
|
+
// drawn one, and the seed it stands under comes back. A ward whose key
|
|
137
|
+
// another ward of this harbor already has does not stand.
|
|
138
|
+
async host(name: string, invitation: Invitation, seed?: string): Promise<string | null> {
|
|
139
|
+
if (this.#wards.has(name)) return null;
|
|
140
|
+
const bytes = seed === undefined ? this.terrain.entropy.draw(32) : isHex(seed, 32) ? unhex(seed) : null;
|
|
141
|
+
if (bytes === null || this.holds((await WardKey.from(bytes)).pk)) return null;
|
|
142
|
+
try {
|
|
143
|
+
await this.#unpack(name, bytes, invitation);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (this.#wards.get(name)!.wardBeing.stance.standings.get(DOCK)) return hex(bytes);
|
|
148
|
+
this.#wards.delete(name);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
keep(): Promise<boolean> {
|
|
153
|
+
return this.box.partition.kept();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Hosting: the terrain's carrier takes a route, or does not dial.
|
|
157
|
+
override route(pk: string, at: string | null): boolean {
|
|
158
|
+
return this.terrain.carrier.route(pk, at);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async #unpack(name: string, seed: Uint8Array, dock?: Invitation, root?: WardParts['root']): Promise<void> {
|
|
162
|
+
const { entropy, clock } = this.terrain;
|
|
163
|
+
const ward = await Ward.unpack({
|
|
164
|
+
seed,
|
|
165
|
+
memory: await this.#package.rows(name),
|
|
166
|
+
catalogue: this,
|
|
167
|
+
entropy,
|
|
168
|
+
clock,
|
|
169
|
+
carrier: this,
|
|
170
|
+
...(dock ? { dock } : {}),
|
|
171
|
+
...(root ? { root } : {}),
|
|
172
|
+
});
|
|
173
|
+
this.#wards.set(name, ward);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#door(pk: string): Ward | undefined {
|
|
177
|
+
for (const ward of this.#wards.values()) if (ward.pk === pk) return ward;
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
package/src/harbor/index.ts
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
|
|
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, the package, the dock, the catalogue and its
|
|
3
|
+
// registry, and the harbor.
|
|
4
|
+
export { Catalogue, CATALOGUE, type Cataloguing, type ModuleRow } from './catalogue.ts';
|
|
5
|
+
export { BOX, Dock, type Hosted, type Hosting } from './dock.ts';
|
|
6
|
+
export { Harbor } from './harbor.ts';
|
|
7
|
+
export { Package } from './package.ts';
|
|
8
|
+
export { readRootRequest, rootLine, type RootRequest } from './root-line.ts';
|
|
9
|
+
export { Registry } from './registry.ts';
|
|
10
|
+
export { Terrain } from './terrain.ts';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The package: a harbor's memory, sealed under the harbor's seed. The
|
|
3
|
+
// terrain keeps its bytes and reads neither a ward's name, nor a being's
|
|
4
|
+
// key, nor anything a row holds.
|
|
5
|
+
//
|
|
6
|
+
// key HKDF-SHA-256 of the seed under `nervur-package`
|
|
7
|
+
// place 16 bytes under ["place", ward], as hex
|
|
8
|
+
// entry 16 bytes under ["entry", ward, row], as hex
|
|
9
|
+
// seal 32 bytes under ["seal", ward]
|
|
10
|
+
// entry bytes nonce (12) || AES-256-GCM( JSON { row, value } ),
|
|
11
|
+
// the entry's name as additional data
|
|
12
|
+
//
|
|
13
|
+
// A row is the head, under the empty name, or one being, under her key. An
|
|
14
|
+
// entry that does not open refuses the whole ward, so nothing unpacks from
|
|
15
|
+
// bytes the package did not seal.
|
|
16
|
+
import { concat, decrypt, encrypt, hex, hkdf, utf8 } from '../crypto/index.ts';
|
|
17
|
+
import type { Entropy, Memory, Rows } from '../contract/index.ts';
|
|
18
|
+
import { HEAD_ROW } from '../ward/index.ts';
|
|
19
|
+
|
|
20
|
+
const NONCE = 12;
|
|
21
|
+
const LABEL = utf8('nervur-package');
|
|
22
|
+
|
|
23
|
+
type Sealed = { row: string; value: unknown };
|
|
24
|
+
|
|
25
|
+
class PackageRows implements Rows {
|
|
26
|
+
readonly rows: Record<string, unknown>;
|
|
27
|
+
readonly #ward: Ward;
|
|
28
|
+
readonly #told = new Set<string>();
|
|
29
|
+
#line: Promise<boolean> = Promise.resolve(true);
|
|
30
|
+
|
|
31
|
+
constructor(ward: Ward, rows: Record<string, unknown>) {
|
|
32
|
+
this.#ward = ward;
|
|
33
|
+
this.rows = rows;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
told(row: string): void {
|
|
37
|
+
this.#told.add(row);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Every row told since the last keep, sealed and written as one keep, one
|
|
41
|
+
// keep at a time.
|
|
42
|
+
kept(): Promise<boolean> {
|
|
43
|
+
const rows = [...this.#told];
|
|
44
|
+
this.#told.clear();
|
|
45
|
+
this.#line = this.#line.then(() => this.#write(rows)).catch(() => false);
|
|
46
|
+
return this.#line;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async #write(told: string[]): Promise<boolean> {
|
|
50
|
+
const beings = (this.rows.beings ?? {}) as Record<string, unknown>;
|
|
51
|
+
const entries = new Map<string, Uint8Array | null>();
|
|
52
|
+
for (const row of told) {
|
|
53
|
+
const value = row === HEAD_ROW ? (this.rows.ward ?? null) : Object.hasOwn(beings, row) ? beings[row] : undefined;
|
|
54
|
+
entries.set(await this.#ward.entry(row), value === undefined ? null : await this.#ward.seal(row, value));
|
|
55
|
+
}
|
|
56
|
+
await this.#ward.write(entries);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class Ward {
|
|
62
|
+
readonly #pack: Package;
|
|
63
|
+
readonly #name: string;
|
|
64
|
+
readonly #seal: Uint8Array;
|
|
65
|
+
readonly place: string;
|
|
66
|
+
|
|
67
|
+
constructor(pack: Package, name: string, seal: Uint8Array, place: string) {
|
|
68
|
+
this.#pack = pack;
|
|
69
|
+
this.#name = name;
|
|
70
|
+
this.#seal = seal;
|
|
71
|
+
this.place = place;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
entry(row: string): Promise<string> {
|
|
75
|
+
return this.#pack.derive(['entry', this.#name, row], 16).then(hex);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async seal(row: string, value: unknown): Promise<Uint8Array> {
|
|
79
|
+
const nonce = this.#pack.entropy.draw(NONCE);
|
|
80
|
+
const text = utf8(JSON.stringify({ row, value } satisfies Sealed));
|
|
81
|
+
return concat(nonce, await encrypt(this.#seal, nonce, utf8(await this.entry(row)), text));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The name is the additional data, so an entry moved under another name
|
|
85
|
+
// does not open either.
|
|
86
|
+
async open(name: string, bytes: Uint8Array): Promise<Sealed> {
|
|
87
|
+
const text = bytes.length > NONCE ? await decrypt(this.#seal, bytes.subarray(0, NONCE), utf8(name), bytes.subarray(NONCE)) : null;
|
|
88
|
+
if (text === null) throw new Error('an entry the package did not seal');
|
|
89
|
+
return JSON.parse(new TextDecoder().decode(text)) as Sealed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
write(entries: ReadonlyMap<string, Uint8Array | null>): Promise<void> {
|
|
93
|
+
return this.#pack.memory.write(this.place, entries);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class Package {
|
|
98
|
+
readonly memory: Memory;
|
|
99
|
+
readonly entropy: Entropy;
|
|
100
|
+
readonly #key: Uint8Array;
|
|
101
|
+
|
|
102
|
+
private constructor(memory: Memory, entropy: Entropy, key: Uint8Array) {
|
|
103
|
+
this.memory = memory;
|
|
104
|
+
this.entropy = entropy;
|
|
105
|
+
this.#key = key;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static async open(memory: Memory, entropy: Entropy, seed: Uint8Array): Promise<Package> {
|
|
109
|
+
return new Package(memory, entropy, await hkdf(seed, LABEL, 32));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
derive(info: string[], length: number): Promise<Uint8Array> {
|
|
113
|
+
return hkdf(this.#key, utf8(JSON.stringify(info)), length);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// One ward's rows, as the package kept them.
|
|
117
|
+
async rows(name: string): Promise<Rows> {
|
|
118
|
+
const ward = new Ward(this, name, await this.derive(['seal', name], 32), hex(await this.derive(['place', name], 16)));
|
|
119
|
+
const rows: Record<string, unknown> = {};
|
|
120
|
+
const beings: Record<string, unknown> = {};
|
|
121
|
+
let any = false;
|
|
122
|
+
for (const [entry, bytes] of await this.memory.read(ward.place)) {
|
|
123
|
+
const { row, value } = await ward.open(entry, bytes);
|
|
124
|
+
any = true;
|
|
125
|
+
if (row === HEAD_ROW) rows.ward = value;
|
|
126
|
+
else beings[row] = value;
|
|
127
|
+
}
|
|
128
|
+
if (any) rows.beings = beings;
|
|
129
|
+
return new PackageRows(ward, rows);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The catalogue's index: every kind the harbor runs, from the kit's own
|
|
3
|
+
// classes and the modules the catalogue loaded. It resolves a kind a row
|
|
4
|
+
// keeps, and decides nothing about what stands. It refuses any set of
|
|
5
|
+
// modules two readings could split:
|
|
6
|
+
//
|
|
7
|
+
// a module whose name is no kind, or is the kit's, or two of one name
|
|
8
|
+
// a class with no kind of its own, or one outside its module's domain
|
|
9
|
+
// two classes of one kind, or one kind in two modules
|
|
10
|
+
// a contract with no kind of its own, or one kind for two contracts
|
|
11
|
+
// a kind that is a class and a contract
|
|
12
|
+
//
|
|
13
|
+
// A module's domain is its name's first two segments: `com.acme.shop`
|
|
14
|
+
// stands classes under `com.acme.` alone. A contract may be any domain's,
|
|
15
|
+
// so a faculty fulfils a contract another module declares. Two harbors hold
|
|
16
|
+
// two registries, and nothing between them names a class.
|
|
17
|
+
import { Faculty, isKind, KIT, kindOf, type BeingClass } from '../being/index.ts';
|
|
18
|
+
import type { Module } from '../contract/index.ts';
|
|
19
|
+
import type { Catalogue } from '../ward/index.ts';
|
|
20
|
+
|
|
21
|
+
const domainOf = (name: string): string => `${name.split('.').slice(0, 2).join('.')}.`;
|
|
22
|
+
|
|
23
|
+
export class Registry implements Catalogue {
|
|
24
|
+
readonly #classes = new Map<string, BeingClass>();
|
|
25
|
+
readonly #modules = new Map<string, string[]>();
|
|
26
|
+
|
|
27
|
+
// `kit` holds the kit's own classes, which alone may be of its kinds.
|
|
28
|
+
constructor(kit: readonly BeingClass[], modules: readonly Module[]) {
|
|
29
|
+
const contracts = new Map<string, unknown>();
|
|
30
|
+
const owners = new Map<string, string>();
|
|
31
|
+
const add = (C: BeingClass, kind: string): void => {
|
|
32
|
+
if (this.#classes.has(kind) && this.#classes.get(kind) !== C) throw new Error(`two classes are of kind ${kind}`);
|
|
33
|
+
this.#classes.set(kind, C);
|
|
34
|
+
};
|
|
35
|
+
for (const C of kit) add(C, kindOf(C));
|
|
36
|
+
for (const m of modules) {
|
|
37
|
+
if (!isKind(m.module) || m.module.startsWith(KIT)) throw new Error(`module ${String(m.module)} is no name a module may take`);
|
|
38
|
+
if (this.#modules.has(m.module)) throw new Error(`two modules are named ${m.module}`);
|
|
39
|
+
if (typeof m.version !== 'string' || m.version === '') throw new Error(`module ${m.module} declares no version`);
|
|
40
|
+
const kinds: string[] = [];
|
|
41
|
+
for (const C of m.classes) {
|
|
42
|
+
if (typeof C !== 'function') throw new Error(`module ${m.module} holds something that is no class`);
|
|
43
|
+
const kind = kindOf(C);
|
|
44
|
+
if (!kind.startsWith(domainOf(m.module))) throw new Error(`kind ${kind} is outside module ${m.module}`);
|
|
45
|
+
const owner = owners.get(kind);
|
|
46
|
+
if (owner !== undefined && owner !== m.module) throw new Error(`kind ${kind} is in modules ${owner} and ${m.module}`);
|
|
47
|
+
owners.set(kind, m.module);
|
|
48
|
+
add(C as BeingClass, kind);
|
|
49
|
+
kinds.push(kind);
|
|
50
|
+
if (!Faculty.fulfils(C)) continue;
|
|
51
|
+
for (let c: unknown = Object.getPrototypeOf(C); c !== Faculty; c = Object.getPrototypeOf(c)) {
|
|
52
|
+
const contract = kindOf(c);
|
|
53
|
+
if (contract.startsWith(KIT)) throw new Error(`kind ${contract} is the kit's`);
|
|
54
|
+
if (contracts.has(contract) && contracts.get(contract) !== c) throw new Error(`two contracts are of kind ${contract}`);
|
|
55
|
+
contracts.set(contract, c);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.#modules.set(m.module, kinds);
|
|
59
|
+
}
|
|
60
|
+
for (const kind of contracts.keys()) {
|
|
61
|
+
if (this.#classes.has(kind)) throw new Error(`kind ${kind} is a class and a contract`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
classOf(kind: string): BeingClass | undefined {
|
|
66
|
+
return this.#classes.get(kind);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The kinds a module stands, or none.
|
|
70
|
+
kindsOf(module: string): string[] {
|
|
71
|
+
return [...(this.#modules.get(module) ?? [])];
|
|
72
|
+
}
|
|
73
|
+
}
|